Compare commits

..

1128 Commits

Author SHA1 Message Date
kennethreitz ce3ab46d59 Bump version to 3.5.0 and update changelog
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 15:27:36 -04:00
kennethreitz 6f9c87d71c Fix broad exception handling and future.result() call
- Call future.result() instead of bare property access in test (#596)
- Catch (ValueError, TypeError) instead of broad Exception in
  response model serialization (#597)
- Catch WebSocketDisconnect instead of broad Exception in
  websocket chat example (#598)

Closes #596, closes #597, closes #598

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 15:25:12 -04:00
kennethreitz 29d0621d98 Replace deprecated asyncio.iscoroutinefunction with inspect.iscoroutinefunction
Closes #599

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 15:23:24 -04:00
kennethreitz 30fa2dfda7 Add uv.lock
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 15:22:06 -04:00
kennethreitz 43c803a426 Restore print statements in lifespan example
These serve as illustrative markers for users reading the example.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 15:21:21 -04:00
Andreas Motl ff6d530338 Chore: Code formatting (#594)
## About
A few cosmetic adjustments aka. code formatting.
Also validate the outcome on CI/GHA.
Feel free to improve now or later at your disposal.

## Details
The updates are based on using the most recent versions of pyproject-fmt
and ruff.
Specifically, spots marked with `noqa` might need further love, also at
your disposal.

---------

Co-authored-by: Kenneth Reitz <me@kennethreitz.org>
2026-03-24 15:21:04 -04:00
kennethreitz a375984310 Fix WSGI mount returning 400 at mount root (#600)
## Summary
- When a WSGI app (e.g. Flask) is mounted at `/prefix` and a request
hits exactly `/prefix`, the path prefix was stripped to `""` instead of
`"/"`, causing frameworks like Flask to return 400.
- One-character fix: default the stripped path to `"/"` when empty.

## Test plan
- [x] `tests/test_responder.py::test_mount_wsgi_app` now passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 15:16:29 -04:00
Andreas Motl 46c6f440c5 Chore: Remove configuration for Read the Docs (#595)
https://responder.kennethreitz.org/ is here to stay.
This patch concludes the decision in GH-564.
2026-03-23 20:19:15 -04:00
Andreas Motl c87e8c876d CI: Validate on Python 3.14 vanilla, free-threaded, and PyPy (#593) 2026-03-23 18:28:27 -04:00
kennethreitz f86c7eed70 Fix RST title underline warning breaking docs CI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:10:51 -04:00
kennethreitz 9d492a383c Add marimo notebook mounting docs and example
- Document mounting marimo ASGI apps in the feature tour
- Add examples/marimo_mount.py showing the integration
- Verified working: marimo.create_asgi_app() mounts cleanly via api.mount()

Fixes #580.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:09:01 -04:00
kennethreitz 77ae49aaef Improve GraphQL API interface, expand tests, drop 3.9 from CI
- Extract variables and operationName from query params and form data,
  not just JSON bodies. Fixes #571.
- Add docstrings to GraphQLView class and methods. Fixes #572.
- Add 10 new GraphQL tests: variables, operationName, mutations,
  context access, malformed queries, raw text, invalid variables
  param. Fixes #568.
- Remove Python 3.9 from CI matrix (dropped in 3.4.0).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:06:00 -04:00
kennethreitz 74c872ed57 Add type annotations to routes.py
Add comprehensive type hints to compile_path, BaseRoute, Route,
WebSocketRoute, and Router classes. Uses Starlette's Scope, Receive,
Send types and properly types the ASGI/WSGI union in Router.apps.
Fixes #566.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 21:47:21 -04:00
kennethreitz 724b769c9e Fix OpenAPI template packaging, add static file tests
- Include ext/openapi/docs/*.html in package_data so OpenAPI docs
  themes (swagger_ui, redoc, rapidoc, elements) ship with the wheel.
  Fixes #583.
- Add tests for static file serving and index.html fallback. Fixes #563.
- Bump version to 3.4.1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 21:44:02 -04:00
kennethreitz 4f02016ed6 Add comprehensive docstrings, expand API reference, upgrade to Starlette 1.0
- Add docstrings to all undocumented public methods across API, Request,
  Response, Router, Route, BackgroundQueue, and related classes
- Expand api.rst with autodoc sections for RouteGroup, BackgroundQueue,
  QueryDict, and RateLimiter
- Update starlette dependency to >=1.0
- Drop Python 3.9 support (required by Starlette 1.0), minimum is now 3.10
- Bump version to 3.4.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 21:37:36 -04:00
kennethreitz 3c2b1acc19 Bump version to 3.3.0 2026-03-22 13:58:18 -04:00
kennethreitz a3b49ab9fd Add auth, WebSocket, middleware, config guides. Examples and changelog.
New documentation pages:
- Authentication: API keys, JWT tokens, session auth, custom exceptions
- WebSocket Tutorial: echo server, chat room, HTML client, data formats
- Writing Middleware: hooks vs middleware, Starlette integration, ordering
- Configuration: env vars, .env files, secret keys, debug mode, production setup

Also:
- Complete working example at end of quickstart with cross-references
- Three new example files: rest_api.py, websocket_chat.py, sse_stream.py
- Changelog updated for v3.2.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:56:35 -04:00
kennethreitz bf17b02653 Add tutorials: REST API, SQLAlchemy, Flask migration. Rewrite CLI and API ref.
Three new tutorial pages:
- Building a REST API: full CRUD with Pydantic validation, from scratch
- Using SQLAlchemy: async engine, lifespan setup, CRUD with ORM
- Migrating from Flask: concept mapping, quick reference table,
  gradual migration via app mounting

Also rewritten:
- CLI docs: cleaner, more concise
- API reference: added prose descriptions for each section

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:34:01 -04:00
kennethreitz 9383cd0f16 Rework homepage prose for better flow
Lead with recognition, earn each paragraph, land on the welcome.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:09:46 -04:00
kennethreitz 226bd63ed3 Full docs pass: educational prose throughout all pages
Every section now teaches web development concepts alongside the code:
- HTTP methods, status codes, content negotiation explained
- What ASGI is and why it matters
- How cookies, sessions, CORS, and HSTS work
- When to use WebSockets vs SSE
- Why request validation matters
- How background tasks differ from task queues
- What rate limiting protects against
- What Host header injection is
- Separation of concerns in templating

People should learn about web development while reading these docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:07:56 -04:00
kennethreitz b3c55f68d9 Expand testing docs with prose, examples, and tips
New sections: request validation, headers/cookies, custom error
handlers, before/after hooks, and practical tips. Every section
now explains the why, not just the how.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:00:29 -04:00
kennethreitz a2a2ae21ff Good intentions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:58:05 -04:00
kennethreitz 84074860aa Add note about Responder's spirit as a passion project
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:55:36 -04:00
kennethreitz 21baa03640 CI: Add CNAME for custom domain in GitHub Pages deploy
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:51:14 -04:00
kennethreitz 0c552e25cb CI: Deploy docs to GitHub Pages on push to main
Uses peaceiris/actions-gh-pages to push built docs to gh-pages branch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:48:05 -04:00
kennethreitz 24958bff51 v3.2.0: Request ID, rate limiting, MessagePack, docs update
New features:
- Request ID: api = responder.API(request_id=True)
- Rate limiting: RateLimiter(requests=100, period=60).install(api)
- MessagePack format: await req.media("msgpack")
- All new features documented in tour

176 tests, 95% coverage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:45:50 -04:00
kennethreitz 364f6b67f7 Add auto-validation, SSE, stream_file, after_request, route groups
Five new features:

1. **Pydantic auto-validation** — if request_model is set, request
   bodies are validated automatically and 422 returned on failure.
   If response_model is set, resp.media is serialized through the
   model (extra fields stripped, types enforced).

2. **Server-Sent Events** — resp.sse for real-time streaming:

       @resp.sse
       async def stream():
           yield {"event": "update", "data": "hello"}

3. **resp.stream_file()** — stream large files without loading
   into memory, with automatic content-type detection.

4. **after_request hooks** — run code after every request:

       @api.after_request()
       def add_request_id(req, resp):
           resp.headers["X-Request-ID"] = str(uuid.uuid4())

5. **Route groups** — organize routes with shared prefixes:

       v1 = api.group("/v1")

       @v1.route("/users")
       def list_users(req, resp): ...

Also fix streaming responses not sending Content-Type headers.

172 tests, 95% coverage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:42:48 -04:00
kennethreitz 2cab7b5af7 Document Pydantic OpenAPI support in feature tour
Three approaches: Pydantic models (recommended), YAML docstrings,
and marshmallow schemas — all work together.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:36:02 -04:00
kennethreitz 1bfd85b003 Add Pydantic support for OpenAPI schema generation
Define your API schemas with Pydantic models instead of (or alongside)
YAML docstrings and marshmallow:

    from pydantic import BaseModel

    class PetIn(BaseModel):
        name: str
        age: int = 0

    class PetOut(BaseModel):
        id: int
        name: str
        age: int

    @api.route("/pets", methods=["POST"],
               request_model=PetIn, response_model=PetOut)
    async def create_pet(req, resp):
        data = await req.media()
        resp.media = {"id": 1, **data}

Also works with @api.schema("Name") decorator for registering
standalone schema components.

Pydantic models, marshmallow schemas, and YAML docstrings can all
be used together in the same API.

Also: rewrite docs with more prose, restore sidebar logo and links,
add FastAPI acknowledgment, update homepage copy.

161 tests, 95% coverage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:35:07 -04:00
kennethreitz 33ebc77f10 Rewrite docs from scratch, remove 14MB of bundled fonts
Complete documentation rewrite:
- Clean, code-first index page (no badges, no testimonials)
- Rewritten quickstart: request/response reference, templates, background tasks
- Rewritten feature tour: method filtering, lifespan, file serving,
  error handling, hooks, WebSockets, GraphQL, OpenAPI, CORS, sessions
- Simplified testing and deployment guides
- Stripped conf.py to essential extensions only

Removed cruft:
- 14MB of paid font files (Mercury, Operator Mono)
- Google Analytics (deprecated Universal Analytics)
- UserVoice widget
- Konami code easter egg
- Algolia DocSearch (not configured)
- Twitter widgets
- Unused Sphinx extensions (mathjax, ifconfig, coverage, doctest, opengraph)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:17:22 -04:00
kennethreitz 30801557a3 Bump version to 3.1.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 07:57:32 -04:00
kennethreitz 73d46e9b03 CI: Remove linkcheck from docs workflow
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 07:54:59 -04:00
kennethreitz 3d65d88ea9 Rewrite README from scratch
Clean, code-first README. No badges, no testimonials — just
show what the framework does with real examples.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 07:53:25 -04:00
kennethreitz 8f979719a0 Remove poethepoet, use direct commands in CI
Replace all poe task runner usage with direct pytest/sphinx/ruff
commands. Remove poethepoet dependency and [tool.poe.tasks] config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 07:50:32 -04:00
kennethreitz 0cbcaf9c4f Code quality improvements and test fixes (#592)
## Summary
Comprehensive post-v3.0.0 modernization: new features, bug fixes,
dependency cleanup, docs, and test coverage.

**New features:**
- **HTTP method filtering** — `@api.route("/data", methods=["GET"])`
- **Lifespan context manager** — modern async startup/shutdown
- **`api.exception_handler()`** — custom error handling per exception
type
- **`api.graphql()`** — one-liner GraphQL setup
- **`resp.file()`** — serve files from disk with auto content-type
- **before_request short-circuit** — set status code to skip route
handler
- **`req.path_params`** / **`req.client`** / **`req.is_json`** — new
request properties
- **`uuid`** and **`path`** route convertors
- **PEP 561 `py.typed`** marker

**Bug fixes:**
- Fix multipart parser losing headers
- Fix `url_for()` with typed params (`{id:int}`)
- Fix `resp.body` encoding crash on bytes content
- Fix Python 3.9 type syntax (`from __future__ import annotations`)
- Fix broken session test and no-op file upload test
- Fix helloworld example 404 on root path

**Dependencies:**
- Flattened — `pip install responder` gets everything
- Core: just starlette + uvicorn (down from 10 deps)

**Docs & README:**
- All new features documented in tour
- Modernized README features list
- Deployment guide: Docker, cloud, uvicorn
- Removed Pipenv, extras, stale references throughout

**Tests & quality:**
- 117 tests (up from 92), 91% coverage, 0 warnings
- CaseInsensitiveDict, GraphQL edge cases, staticfiles tests
- Ruff clean, all `tmpdir` → `tmp_path`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 07:44:11 -04:00
kennethreitz 3fa6f11ffa Clean up stale comments, dead test code, and flaky npm assertions
- Remove commented-out tests (route overlap, form data, file uploads)
- Remove stale TODO/FIXME comments from routes.py and api.py
- Make CLI npm error assertions case-insensitive and more flexible
  to handle different npm versions across CI environments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 8b88b148bf Remove requests/requests-toolbelt from test code
Replace with stdlib urllib.request. No third-party HTTP client
needed for test probing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 1aecafa82a Update CHANGELOG for v3.0.0 release
Reflect all changes made in this branch: dependency reduction,
GraphQL modernization, pyproject.toml migration, etc. Also fix
compare links to point to kennethreitz/responder.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 8c763aa97e Migrate from setup.py to declarative pyproject.toml
All package metadata now lives in pyproject.toml. Removes setup.py
and MANIFEST.in. Also exports __version__ from the package root.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 91aa242a5a Fix python-multipart import deprecation warning
Use `python_multipart` import name instead of deprecated `multipart`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 084d057a99 Move apispec and marshmallow to openapi extra
These are only used by the OpenAPI extension, not core responder.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz d3acf2c1c1 Drop rfc3986 dep, clean up internals
- Replace rfc3986 with stdlib urllib.parse
- Remove deprecated status code aliases (resume_incomplete/resume)
  that were marked for removal in 3.0
- Remove private ThreadPoolExecutor API usage in BackgroundQueue
- Clean up stale comments (old Starlette PR refs, requests attribution)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 80715a12ac Drop requests dependency, modernize GraphQL, clean up setup.py
- Remove `requests` as a core dependency — replaced CaseInsensitiveDict
  and RequestsCookieJar with lightweight stdlib implementations
- Remove `requests-toolbelt` — replaced multipart decoder with
  python-multipart (already a starlette transitive dep)
- Upgrade GraphQL to graphene 3 + graphql-core 3, drop graphql-server-core
- Update GraphiQL template from 0.12.0 (2018) to 3.0.6 with React 18
- Clean up setup.py: remove dead DebCommand, UploadCommand, publish hack
- Remove linting from `poe check` (tests only)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 66fc7afbe4 CI: Simplify test matrix to Ubuntu-only
No need to test on all three OSes — this is a pure Python web
framework with no platform-specific code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz e7776eb9e8 v3.0.0: Modernize for latest Starlette, drop EOL Pythons
- Bump version to 3.0.0
- Replace deprecated starlette.middleware.wsgi with a2wsgi
- Pin starlette[full]>=0.40, add a2wsgi dependency
- Bump minimum Python to 3.9, drop 3.7/3.8 support
- Remove whitenoise conditional (Python <3.8 no longer supported)
- Clean up CI matrix: drop old Python versions and OS exclusions
- Fix deprecated httpx TestClient usage in tests (data= -> content=, per-request cookies)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
Andreas Motl 944d47da45 CI: Remove pip caching. Has woes. 2025-02-03 00:43:10 +01:00
Andreas Motl a3a12cff77 Chore: Add another basic example 2025-02-03 00:43:10 +01:00
Andreas Motl 7b2839086d Thanks, JetBrains. 2025-01-21 23:19:09 +01:00
Andreas Motl 351ff8d95e Documentation: Copy editing, this and that 2025-01-19 05:21:46 +01:00
Andreas Motl 2278beba18 SFA: Update to pueblo[sfa] 0.0.11 2025-01-19 05:02:47 +01:00
Andreas Motl 3cfc7ec2b6 Chore: Remove support for EOL Python 3.6 2025-01-19 05:02:47 +01:00
Andreas Motl 0de22eeed2 SFA: Use application loader from pueblo.sfa 2025-01-19 05:02:47 +01:00
Andreas Motl b0cc37861b SFA: Unlock loading application from remote location, using fsspec 2025-01-19 05:02:47 +01:00
Andreas Motl 7d4532acc9 CI: Use GHA recipe astral-sh/setup-uv, and more 2025-01-18 22:56:50 +01:00
Andreas Motl 1b63d2943a Chore: A few updates from code review etc. 2025-01-18 22:22:36 +01:00
Andreas Motl b5723303c8 CI: The macOS-12 environment is deprecated 2025-01-18 22:22:36 +01:00
Andreas Motl 5730be4b31 Chore: Format code using most recent ruff and pyproject-fmt 2025-01-18 22:22:36 +01:00
Andreas Motl 6f9c11645a CLI: Load from file or module. Add software tests and documentation.
Also, refactor to `responder.ext.cli`.
2025-01-18 22:22:36 +01:00
kennethreitz 827cc64988 CLI: Re-add command line interface (2024)
Install: pip install 'responder[cli]'

The CLI is an optional subsystem from now on.
2025-01-18 22:22:36 +01:00
Andreas Motl 7b5db5bc33 uvicorn: Fix uvicorn.run invocation re. debug argument
The `debug` argument no longer exists. Let's adjust the `log_level` to
`debug` instead.
2025-01-18 22:22:36 +01:00
kennethreitz b9a03c7088 Create FUNDING.yml
Signed-off-by: Kenneth Reitz <me@kennethreitz.org>
2024-11-17 06:28:31 -05:00
Andreas Motl 4cbf55508e Documentation: Update change log for upcoming version 3.0.0 2024-10-31 09:51:29 +01:00
Andreas Motl 83d0fcf1ae Documentation: Update developer sandbox documentation 2024-10-31 09:51:29 +01:00
Taoufik a698eaaab3 GraphQL: Re-add extension and dependencies (2024) 2024-10-31 06:36:13 +01:00
Andreas Motl 3aa21eed08 OpenAPI: Refactor module to responder.ext.openapi
It has been `responder.ext.schema` before.
2024-10-30 23:45:55 +01:00
Andreas Motl 2741c74b90 OpenAPI: Make extension optional
Install with: pip install 'responder[openapi]'
2024-10-30 23:45:55 +01:00
Andreas Motl aba96525ad Dependencies: Migrate from WhiteNoise to ServeStatic 2024-10-30 23:21:23 +01:00
Andreas Motl a5b6d36991 Sandbox: Enable mypy type checker 2024-10-30 23:12:11 +01:00
Andreas Motl e4cff76fa6 Documentation: Unlock writing in Markdown, using Sphinx/MyST 2024-10-30 21:23:12 +01:00
Andreas Motl f11ad7136d Documentation: Add Sphinx extensions "copybutton" and "opengraph" 2024-10-30 21:23:12 +01:00
Andreas Motl c32e8c7468 Documentation: Refactor Sphinx dependencies into setup.py 2024-10-30 20:42:10 +01:00
Andreas Motl d93e3cd12c Documentation: Update Read the Docs (RTD) configuration 2024-10-30 20:42:10 +01:00
Andreas Motl 040f1a57e4 Dependencies: Remove aiofiles
Apparently, it is not used.
2024-10-28 16:36:46 +01:00
dependabot[bot] 307313744f Update alabaster requirement from <0.8 to <1.1
Updates the requirements on [alabaster](https://github.com/sphinx-doc/alabaster) to permit the latest version.
- [Release notes](https://github.com/sphinx-doc/alabaster/releases)
- [Changelog](https://github.com/sphinx-doc/alabaster/blob/master/docs/changelog.rst)
- [Commits](https://github.com/sphinx-doc/alabaster/compare/0.1.0...1.0.0)

---
updated-dependencies:
- dependency-name: alabaster
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-28 10:47:51 +01:00
Andreas Motl 98ca45003b Documentation: Badges, linking, wording, inline comments. This and that.
A few of the adjustments here have been required to mitigate Sphinx
warnings, which would converge to errors on CI, thus failing the build.

A few other changes, both wording and syntax/formatting fixes, are
coming from regular copyediting and documentation maintenance.
2024-10-27 18:13:13 +01:00
Andreas Motl ab76594297 CI: Run link checker and build documentation as GHA workflow 2024-10-27 18:13:13 +01:00
Andreas Motl 7fba0f6362 Fix dispatching static_route=None on Windows 2024-10-26 05:20:57 -04:00
Andreas Motl 4ff73e9d0c Sandbox: Bring back python setup.py publish subcommand
It has been removed too early.
2024-10-26 05:16:12 -04:00
Andreas Motl 68bbea0a55 CI: Validate on Python 3.6+
You never know how you possibly save someone's life with that.
2024-10-26 00:42:07 +02:00
Andreas Motl 106e5e9073 CI: Validate on Windows operating system 2024-10-26 00:27:44 +02:00
Andreas Motl 3426aa71da Documentation: Fix broken links in README 2024-10-25 12:13:08 -04:00
Andreas Motl 413028b636 Tasks: Define sandbox tasks in pyproject.toml, using poethepoet
The fundamental commands to mostly use are:

- poe format
- poe check
2024-10-25 07:39:54 -04:00
Andreas Motl 3edf979a8c Dependencies: Dissolve requirements-dev.txt 2024-10-25 07:39:54 -04:00
Andreas Motl cd75deeb4e Python: Verify support for Python 3.13 2024-10-24 18:36:05 +02:00
Andreas Motl b71bb5ddb9 apistar: Rename variables api_theme -> openapi_theme, etc. 2024-10-24 18:19:03 +02:00
Tabot Kevin 27a9459f22 apistar: Replace use of apistar package with local API theme files
This has already been submitted by @tabotkevin with GH-480, but got lost
for whatever reason.
2024-10-24 18:19:03 +02:00
dependabot[bot] b39c539d57 Update readme-renderer requirement from <23 to <45 (#540)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Updates the requirements on
[readme-renderer](https://github.com/pypa/readme_renderer) to permit the
latest version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/pypa/readme_renderer/releases">readme-renderer's
releases</a>.</em></p>
<blockquote>
<h2>44.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Support newer docutils versions by <a
href="https://github.com/kurtmckee"><code>@​kurtmckee</code></a> in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/315">pypa/readme_renderer#315</a></li>
<li>Resolve Node 16 deprecation warnings in CI by <a
href="https://github.com/kurtmckee"><code>@​kurtmckee</code></a> in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/309">pypa/readme_renderer#309</a></li>
<li>Lint specific directories by <a
href="https://github.com/kurtmckee"><code>@​kurtmckee</code></a> in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/312">pypa/readme_renderer#312</a></li>
<li>Build a wheel once, for all test environments by <a
href="https://github.com/kurtmckee"><code>@​kurtmckee</code></a> in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/308">pypa/readme_renderer#308</a></li>
<li>Update .gitpod.yml to replace deprecated extension by <a
href="https://github.com/shenxianpeng"><code>@​shenxianpeng</code></a>
in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/306">pypa/readme_renderer#306</a></li>
<li>Exclude .gitpod.yml by default with check-manifest by <a
href="https://github.com/shenxianpeng"><code>@​shenxianpeng</code></a>
in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/307">pypa/readme_renderer#307</a></li>
<li>Lazy open output files, and always close them by <a
href="https://github.com/kurtmckee"><code>@​kurtmckee</code></a> in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/314">pypa/readme_renderer#314</a></li>
<li>Release 44 by <a
href="https://github.com/kurtmckee"><code>@​kurtmckee</code></a> in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/316">pypa/readme_renderer#316</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/kurtmckee"><code>@​kurtmckee</code></a>
made their first contribution in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/315">pypa/readme_renderer#315</a></li>
<li><a
href="https://github.com/shenxianpeng"><code>@​shenxianpeng</code></a>
made their first contribution in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/306">pypa/readme_renderer#306</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/pypa/readme_renderer/compare/43.0...44.0">https://github.com/pypa/readme_renderer/compare/43.0...44.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/pypa/readme_renderer/blob/main/CHANGES.rst">readme-renderer's
changelog</a>.</em></p>
<blockquote>
<h2>44.0 (2024-07-08)</h2>
<ul>
<li>Drop support for Python 3.8 (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/315">#315</a>)</li>
<li>Require docutils 0.21.2 and higher (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/315">#315</a>)</li>
<li>Remove HTML5 <code>&lt;s&gt;</code> tag from the list of allowed
HTML tags (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/315">#315</a>)</li>
<li>Test all supported CPython and PyPy versions in CI (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/315">#315</a>)</li>
<li>Resolve Node 16 deprecation warnings in CI (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/309">#309</a>)</li>
<li>Lint specific directories (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/312">#312</a>)</li>
<li>Build a wheel once for all tox test environments (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/308">#308</a>)</li>
<li>Lazy open output files, and always close them (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/314">#314</a>)</li>
<li>Gitpod: Migrate to the Even Better TOML extension (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/306">#306</a>)</li>
<li>check-manifest: Remove a now-default <code>.gitpod.yml</code>
exclusion (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/307">#307</a>)</li>
</ul>
<h2>43.0 (2024-02-26)</h2>
<ul>
<li>Allow HTML5 <code>picture</code> tag through cleaner (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/299">#299</a>)</li>
<li>Test against Python 3.12 (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/300">#300</a>)</li>
</ul>
<h2>42.0 (2023-09-07)</h2>
<ul>
<li>Migrate from <code>bleach</code> to <code>nh3</code> (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/295">#295</a>)</li>
<li>Migrate from <code>setup.py</code> to
<code>pyproject.toml</code></li>
</ul>
<h2>41.0 (2023-08-18)</h2>
<ul>
<li>Allow HTML5 <code>figcaption</code> tag through cleaner (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/291">#291</a>)</li>
<li>Test <code>README.rst</code> from this project (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/288">#288</a>)</li>
</ul>
<h2>40.0 (2023-06-16)</h2>
<ul>
<li>Add CLI option to render package README. (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/271">#271</a>)</li>
<li>Adapt tests to pygments 2.14.0 (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/272">#272</a>)</li>
<li>Update release process to use Trusted Publishing (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/276">#276</a>)</li>
<li>Replace usage of deprecated <code>pkg_resources</code> with
<code>importlib.metadata</code> (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/281">#281</a>)</li>
<li>Drop support for Python 3.7 (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/282">#282</a>),
Test against Python 3.11 (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/280">#280</a>)</li>
</ul>
<h2>37.3 (2022-10-31)</h2>
<ul>
<li>Allow HTML5 <code>figure</code> tag through cleaner (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/265">#265</a>)</li>
</ul>
<h2>37.2 (2022-09-24)</h2>
<ul>
<li>Allow HTML5 <code>s</code> tag through cleaner (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/261">#261</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/pypa/readme_renderer/commit/1d0497c37a6033d791c74e800590dcd0d34f6e08"><code>1d0497c</code></a>
Release 44 (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/316">#316</a>)</li>
<li><a
href="https://github.com/pypa/readme_renderer/commit/09620a64219f80238e396b25ab18016ca495cf3c"><code>09620a6</code></a>
Lazy open output files, and always close them (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/314">#314</a>)</li>
<li><a
href="https://github.com/pypa/readme_renderer/commit/6061b3ebbcdecc33f369c0d48fe0641b34858294"><code>6061b3e</code></a>
Exclude .gitpod.yml by default with check-manifest (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/307">#307</a>)</li>
<li><a
href="https://github.com/pypa/readme_renderer/commit/749204b0eaa5a72fceb29c707d5321687ac447a3"><code>749204b</code></a>
Update .gitpod.yml to replace deprecated extension (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/306">#306</a>)</li>
<li><a
href="https://github.com/pypa/readme_renderer/commit/e84ded18e61e33ae117f20d0eefb1f92edc88ed0"><code>e84ded1</code></a>
Build a wheel once, for all test environments (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/308">#308</a>)</li>
<li><a
href="https://github.com/pypa/readme_renderer/commit/b447d5d7ba60ee71dff19f753d7b6c33312411b8"><code>b447d5d</code></a>
Lint specific directories (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/312">#312</a>)</li>
<li><a
href="https://github.com/pypa/readme_renderer/commit/08172046a88d019989c2de36f9cc0c88695cf2b2"><code>0817204</code></a>
Resolve Node 16 deprecation warnings in CI (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/309">#309</a>)</li>
<li><a
href="https://github.com/pypa/readme_renderer/commit/fefd2859fb3253744a21f327b2079cdd14240bfe"><code>fefd285</code></a>
Support newer docutils versions (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/315">#315</a>)</li>
<li><a
href="https://github.com/pypa/readme_renderer/commit/175c65ad514acad7fccf54c3aab9fe701bdd9f06"><code>175c65a</code></a>
Release 43.0 (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/303">#303</a>)</li>
<li><a
href="https://github.com/pypa/readme_renderer/commit/78ccf3f50f58467e2b3886412d3ba8c7a3a398d4"><code>78ccf3f</code></a>
adds testing for 3.12, fixes <a
href="https://redirect.github.com/pypa/readme_renderer/issues/290">#290</a>
(<a
href="https://redirect.github.com/pypa/readme_renderer/issues/300">#300</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/pypa/readme_renderer/compare/0.1.0...44.0">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-24 07:39:51 -04:00
dependabot[bot] 718b53cce2 Update markupsafe requirement from <2 to <4 (#539)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Updates the requirements on
[markupsafe](https://github.com/pallets/markupsafe) to permit the latest
version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/pallets/markupsafe/releases">markupsafe's
releases</a>.</em></p>
<blockquote>
<h2>3.0.2</h2>
<p>This is the MarkupSafe 3.0.2 fix release, which fixes bugs but does
not otherwise change behavior and should not result in breaking
changes.</p>
<p>PyPI: <a
href="https://pypi.org/project/MarkupSafe/3.0.2/">https://pypi.org/project/MarkupSafe/3.0.2/</a>
Changes: <a
href="https://markupsafe.palletsprojects.com/en/stable/changes/#version-3-0-2">https://markupsafe.palletsprojects.com/en/stable/changes/#version-3-0-2</a>
Milestone: <a
href="https://github.com/pallets/markupsafe/milestone/14?closed=1">https://github.com/pallets/markupsafe/milestone/14?closed=1</a></p>
<ul>
<li>Fix compatibility when <code>__str__</code> returns a
<code>str</code> subclass. <a
href="https://redirect.github.com/pallets/markupsafe/issues/472">#472</a></li>
<li>Build requires setuptools &gt;= 70.1. <a
href="https://redirect.github.com/pallets/markupsafe/issues/475">#475</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/pallets/markupsafe/blob/main/CHANGES.rst">markupsafe's
changelog</a>.</em></p>
<blockquote>
<h2>Version 3.0.2</h2>
<p>Released 2024-10-18</p>
<ul>
<li>Fix compatibility when <code>__str__</code> returns a
<code>str</code> subclass. :issue:<code>472</code></li>
<li>Build requires setuptools &gt;= 70.1. :issue:<code>475</code></li>
</ul>
<h2>Version 3.0.1</h2>
<p>Released 2024-10-08</p>
<ul>
<li>Address compiler warnings that became errors in GCC 14.
:issue:<code>466</code></li>
<li>Fix compatibility with proxy objects. :issue:<code>467</code></li>
</ul>
<h2>Version 3.0.0</h2>
<p>Released 2024-10-07</p>
<ul>
<li>Support Python 3.13 and its experimental free-threaded build.
:pr:<code>461</code></li>
<li>Drop support for Python 3.7 and 3.8.</li>
<li>Use modern packaging metadata with <code>pyproject.toml</code>
instead of <code>setup.cfg</code>.
:pr:<code>348</code></li>
<li>Change <code>distutils</code> imports to <code>setuptools</code>.
:pr:<code>399</code></li>
<li>Use deferred evaluation of annotations. :pr:<code>400</code></li>
<li>Update signatures for <code>Markup</code> methods to match
<code>str</code> signatures. Use
positional-only arguments. :pr:<code>400</code></li>
<li>Some <code>str</code> methods on <code>Markup</code> no longer
escape their argument:
<code>strip</code>, <code>lstrip</code>, <code>rstrip</code>,
<code>removeprefix</code>, <code>removesuffix</code>,
<code>partition</code>, and <code>rpartition</code>;
<code>replace</code> only escapes its <code>new</code>
argument. These methods are conceptually linked to search methods such
as
<code>in</code>, <code>find</code>, and <code>index</code>, which
already do not escape their argument.
:issue:<code>401</code></li>
<li>The <code>__version__</code> attribute is deprecated. Use feature
detection, or
<code>importlib.metadata.version(&quot;markupsafe&quot;)</code>,
instead. :pr:<code>402</code></li>
<li>Speed up escaping plain strings by 40%. :pr:<code>434</code></li>
<li>Simplify speedups implementation. :pr:<code>437</code></li>
</ul>
<h2>Version 2.1.5</h2>
<p>Released 2024-02-02</p>
<ul>
<li>Fix <code>striptags</code> not collapsing spaces.
:issue:<code>417</code></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/pallets/markupsafe/commit/28ace20b140d15c083e1cbc163ee6b7778ba098c"><code>28ace20</code></a>
release version 3.0.2</li>
<li><a
href="https://github.com/pallets/markupsafe/commit/6b51fd8f7386983b7038ad973557367cbd48579a"><code>6b51fd8</code></a>
build requires at least setuptools 70.1 (<a
href="https://redirect.github.com/pallets/markupsafe/issues/478">#478</a>)</li>
<li><a
href="https://github.com/pallets/markupsafe/commit/99dda9fd708432bd07d02327b2668661aa3cdaa0"><code>99dda9f</code></a>
build requires at least setuptools 70.1</li>
<li><a
href="https://github.com/pallets/markupsafe/commit/3d8fd8cc006124a49ce2f4268b4d1739e301583e"><code>3d8fd8c</code></a>
fix version</li>
<li><a
href="https://github.com/pallets/markupsafe/commit/1933c4be9c2c88613f7660840cde27a1bb7567e0"><code>1933c4b</code></a>
fix version</li>
<li><a
href="https://github.com/pallets/markupsafe/commit/e85aff4d878aa458d5c1e879bf475d8483647f71"><code>e85aff4</code></a>
relax speedups str check (<a
href="https://redirect.github.com/pallets/markupsafe/issues/477">#477</a>)</li>
<li><a
href="https://github.com/pallets/markupsafe/commit/8cb1691ca038ca39942e088b956f5b94d8f636bf"><code>8cb1691</code></a>
relax speedups str check</li>
<li><a
href="https://github.com/pallets/markupsafe/commit/4dafb7c36f1f654f1edd85228d346252b0065d45"><code>4dafb7c</code></a>
start version 3.1.0</li>
<li><a
href="https://github.com/pallets/markupsafe/commit/9c44ecf45141f691d373a66ce664c43b5a6cc761"><code>9c44ecf</code></a>
update docs build</li>
<li><a
href="https://github.com/pallets/markupsafe/commit/275c76905617c3f0e34de14e8794fcf4dfb0f937"><code>275c769</code></a>
Merge branch '2.1.x' into 3.0.x</li>
<li>Additional commits viewable in <a
href="https://github.com/pallets/markupsafe/compare/0.9...3.0.2">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-24 07:39:11 -04:00
dependabot[bot] 2e0b4975f7 Update sphinxcontrib-websupport requirement from <1.2 to <2.1 (#538)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Updates the requirements on
[sphinxcontrib-websupport](https://github.com/sphinx-doc/sphinxcontrib-websupport)
to permit the latest version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/releases">sphinxcontrib-websupport's
releases</a>.</em></p>
<blockquote>
<h2>sphinxcontrib-websupport 2.0.0</h2>
<p>Changelog: <a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/blob/master/CHANGES.rst">https://github.com/sphinx-doc/sphinxcontrib-websupport/blob/master/CHANGES.rst</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/blob/master/CHANGES.rst">sphinxcontrib-websupport's
changelog</a>.</em></p>
<blockquote>
<h1>Release 2.0.0 (2024-07-28)</h1>
<ul>
<li>Adopt Ruff</li>
<li>Tighten MyPy settings</li>
<li>Update GitHub actions versions</li>
</ul>
<h1>Release 1.2.7 (2024-01-13)</h1>
<ul>
<li>Fix tests for sqlalchemy 2.</li>
<li>Publish a <code>whoosh</code> extra.</li>
</ul>
<h1>Release 1.2.6 (2023-08-09)</h1>
<ul>
<li>Fix tests for Sphinx 7.1 and below</li>
</ul>
<h1>Release 1.2.5 (2023-08-07)</h1>
<ul>
<li>Drop support for Python 3.5, 3.6, 3.7, and 3.8</li>
<li>Raise minimum required Sphinx version to 5.0</li>
</ul>
<h1>Release 1.2.4 (2020-08-09)</h1>
<ul>
<li>Import PickleHTMLBuilder from sphinxcontrib-serializinghtml
package</li>
</ul>
<h1>Release 1.2.3 (2020-06-27)</h1>
<ul>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinxcontrib-websupport/issues/43">#43</a>:
doctreedir argument has been ignored on initialize app</li>
</ul>
<h1>Release 1.2.2 (2020-04-29)</h1>
<ul>
<li>Stop to use sphinx.util.pycompat:htmlescape</li>
</ul>
<h1>Release 1.2.1 (2020-03-21)</h1>
<ul>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinxcontrib-websupport/issues/41">#41</a>:
templates/searchresults.html is missing in the source tarball</li>
</ul>
<h1>Release 1.2.0 (2020-02-07)</h1>
<ul>
<li>Drop python2.7 and 3.4 support</li>
</ul>
<p>Release 1.1.2 (2019-05-19)</p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/142b41e404e0197c8b48147284302cb6aa8b4207"><code>142b41e</code></a>
Bump to 2.0.0</li>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/6a625bd314a7338c3a91ebdf846743421387092d"><code>6a625bd</code></a>
Update CHANGES links</li>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/b6906da79bfff09120d43a0aa551b89d774ea3af"><code>b6906da</code></a>
Rename LICENSE to LICENCE.rst</li>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/a21e38577951121bd92f5a5606a012dc75a0a32b"><code>a21e385</code></a>
Rename CHANGES to CHANGES.rst</li>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/992d6fd2fd4ed1185d424596517a0c81be6c039b"><code>992d6fd</code></a>
Run CI with Python 3.12 releases</li>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/6c0277eb35aa2866b18d9bdc111fc074719309f0"><code>6c0277e</code></a>
Run mypy without command-line options</li>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/83f178dcc1e446956d8a24de06afe8222dc48e1b"><code>83f178d</code></a>
Use the latest GitHub actions versions</li>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/155ae9ca9f26c022d79f93058da75b7385e1adf1"><code>155ae9c</code></a>
Enable GitHub's dependabot package update service</li>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/7f05400e51a9bae54009f90cc60dbee83341c087"><code>7f05400</code></a>
Adopt Ruff and use stricter MyPy settings</li>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/be777a7ed355ac85234646505c6ce402966d7543"><code>be777a7</code></a>
Update .gitignore</li>
<li>Additional commits viewable in <a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/compare/1.0.0...2.0.0">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kenneth Reitz <me@kennethreitz.org>
2024-10-24 07:38:58 -04:00
dependabot[bot] a118a5dc4b Bump actions/setup-python from 4 to 5 (#537)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps [actions/setup-python](https://github.com/actions/setup-python)
from 4 to 5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/setup-python/releases">actions/setup-python's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<p>In scope of this release, we update node version runtime from node16
to node20 (<a
href="https://redirect.github.com/actions/setup-python/pull/772">actions/setup-python#772</a>).
Besides, we update dependencies to the latest versions.</p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-python/compare/v4.8.0...v5.0.0">https://github.com/actions/setup-python/compare/v4.8.0...v5.0.0</a></p>
<h2>v4.8.0</h2>
<h2>What's Changed</h2>
<p>In scope of this release we added support for GraalPy (<a
href="https://redirect.github.com/actions/setup-python/pull/694">actions/setup-python#694</a>).
You can use this snippet to set up GraalPy:</p>
<pre lang="yaml"><code>steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4 
  with:
    python-version: 'graalpy-22.3' 
- run: python my_script.py
</code></pre>
<p>Besides, the release contains such changes as:</p>
<ul>
<li>Trim python version when reading from file by <a
href="https://github.com/FerranPares"><code>@​FerranPares</code></a> in
<a
href="https://redirect.github.com/actions/setup-python/pull/628">actions/setup-python#628</a></li>
<li>Use non-deprecated versions in examples by <a
href="https://github.com/jeffwidman"><code>@​jeffwidman</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/724">actions/setup-python#724</a></li>
<li>Change deprecation comment to past tense by <a
href="https://github.com/jeffwidman"><code>@​jeffwidman</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/723">actions/setup-python#723</a></li>
<li>Bump <code>@​babel/traverse</code> from 7.9.0 to 7.23.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/743">actions/setup-python#743</a></li>
<li>advanced-usage.md: Encourage the use actions/checkout@v4 by <a
href="https://github.com/cclauss"><code>@​cclauss</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/729">actions/setup-python#729</a></li>
<li>Examples now use checkout@v4 by <a
href="https://github.com/simonw"><code>@​simonw</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/738">actions/setup-python#738</a></li>
<li>Update actions/checkout to v4 by <a
href="https://github.com/dmitry-shibanov"><code>@​dmitry-shibanov</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/761">actions/setup-python#761</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/FerranPares"><code>@​FerranPares</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-python/pull/628">actions/setup-python#628</a></li>
<li><a href="https://github.com/timfel"><code>@​timfel</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-python/pull/694">actions/setup-python#694</a></li>
<li><a
href="https://github.com/jeffwidman"><code>@​jeffwidman</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-python/pull/724">actions/setup-python#724</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-python/compare/v4...v4.8.0">https://github.com/actions/setup-python/compare/v4...v4.8.0</a></p>
<h2>v4.7.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Bump word-wrap from 1.2.3 to 1.2.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/702">actions/setup-python#702</a></li>
<li>Add range validation for toml files by <a
href="https://github.com/dmitry-shibanov"><code>@​dmitry-shibanov</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/726">actions/setup-python#726</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-python/compare/v4...v4.7.1">https://github.com/actions/setup-python/compare/v4...v4.7.1</a></p>
<h2>v4.7.0</h2>
<p>In scope of this release, the support for reading python version from
pyproject.toml was added (<a
href="https://redirect.github.com/actions/setup-python/pull/669">actions/setup-python#669</a>).</p>
<pre lang="yaml"><code>      - name: Setup Python
        uses: actions/setup-python@v4
&lt;/tr&gt;&lt;/table&gt; 
</code></pre>
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/setup-python/commit/f677139bbe7f9c59b41e40162b753c062f5d49a3"><code>f677139</code></a>
Bump pyinstaller from 3.6 to 5.13.1 in /<strong>tests</strong>/data (<a
href="https://redirect.github.com/actions/setup-python/issues/923">#923</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/2bd53f9a4d1dd1cd21eaffcc01a7b91a8e73ea4c"><code>2bd53f9</code></a>
Documentation update for caching poetry dependencies (<a
href="https://redirect.github.com/actions/setup-python/issues/908">#908</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/80b49d3ed89312896dbdcbefc2ddb159c7f8ca43"><code>80b49d3</code></a>
fix: add arch to cache key (<a
href="https://redirect.github.com/actions/setup-python/issues/896">#896</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/036a5236741fd24c89eea80d1b76179e8e5f9214"><code>036a523</code></a>
Fix: Add <code>.zip</code> extension to Windows package downloads for
<code>Expand-Archive</code> C...</li>
<li><a
href="https://github.com/actions/setup-python/commit/04c1311429f7be71707d8ab66c7af8a14e54b938"><code>04c1311</code></a>
Fix display of emojis in contributors doc (<a
href="https://redirect.github.com/actions/setup-python/issues/899">#899</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/cb6845644151e35f879e10f2f0896c3c8bee372c"><code>cb68456</code></a>
Updated <code>@​iarna/toml</code> version to 3.0.0 (<a
href="https://redirect.github.com/actions/setup-python/issues/912">#912</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/39cd14951b08e74b54015e9e001cdefcf80e669f"><code>39cd149</code></a>
Documentation update for cache (<a
href="https://redirect.github.com/actions/setup-python/issues/873">#873</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/a0d74c0c423f896bc4e7be91d5cb1e2d54438db3"><code>a0d74c0</code></a>
fix(ci): update all failing workflows (<a
href="https://redirect.github.com/actions/setup-python/issues/863">#863</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/4eb7dbcb9561cb76a85079ffa9d89b983166e00c"><code>4eb7dbc</code></a>
Bump braces from 3.0.2 to 3.0.3 (<a
href="https://redirect.github.com/actions/setup-python/issues/893">#893</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/82c7e631bb3cdc910f68e0081d67478d79c6982d"><code>82c7e63</code></a>
Documentation changes for avoiding rate limit issues on GHES (<a
href="https://redirect.github.com/actions/setup-python/issues/835">#835</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/setup-python/compare/v4...v5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Kenneth Reitz <me@kennethreitz.org>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kenneth Reitz <me@kennethreitz.org>
2024-10-24 07:38:37 -04:00
dependabot[bot] 69c1d7f185 Bump actions/checkout from 3 to 4 (#536)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to
4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v4.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update default runtime to node20 by <a
href="https://github.com/takost"><code>@​takost</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1436">actions/checkout#1436</a></li>
<li>Support fetching without the --progress option by <a
href="https://github.com/simonbaird"><code>@​simonbaird</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1067">actions/checkout#1067</a></li>
<li>Release 4.0.0 by <a
href="https://github.com/takost"><code>@​takost</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1447">actions/checkout#1447</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/takost"><code>@​takost</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1436">actions/checkout#1436</a></li>
<li><a
href="https://github.com/simonbaird"><code>@​simonbaird</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1067">actions/checkout#1067</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v3...v4.0.0">https://github.com/actions/checkout/compare/v3...v4.0.0</a></p>
<h2>v3.6.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Mark test scripts with Bash'isms to be run via Bash by <a
href="https://github.com/dscho"><code>@​dscho</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1377">actions/checkout#1377</a></li>
<li>Add option to fetch tags even if fetch-depth &gt; 0 by <a
href="https://github.com/RobertWieczoreck"><code>@​RobertWieczoreck</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/579">actions/checkout#579</a></li>
<li>Release 3.6.0 by <a
href="https://github.com/luketomlinson"><code>@​luketomlinson</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1437">actions/checkout#1437</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/RobertWieczoreck"><code>@​RobertWieczoreck</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/579">actions/checkout#579</a></li>
<li><a
href="https://github.com/luketomlinson"><code>@​luketomlinson</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1437">actions/checkout#1437</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v3.5.3...v3.6.0">https://github.com/actions/checkout/compare/v3.5.3...v3.6.0</a></p>
<h2>v3.5.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix: Checkout Issue in self hosted runner due to faulty submodule
check-ins by <a
href="https://github.com/megamanics"><code>@​megamanics</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1196">actions/checkout#1196</a></li>
<li>Fix typos found by codespell by <a
href="https://github.com/DimitriPapadopoulos"><code>@​DimitriPapadopoulos</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1287">actions/checkout#1287</a></li>
<li>Add support for sparse checkouts by <a
href="https://github.com/dscho"><code>@​dscho</code></a> and <a
href="https://github.com/dfdez"><code>@​dfdez</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1369">actions/checkout#1369</a></li>
<li>Release v3.5.3 by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1376">actions/checkout#1376</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/megamanics"><code>@​megamanics</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1196">actions/checkout#1196</a></li>
<li><a
href="https://github.com/DimitriPapadopoulos"><code>@​DimitriPapadopoulos</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1287">actions/checkout#1287</a></li>
<li><a href="https://github.com/dfdez"><code>@​dfdez</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1369">actions/checkout#1369</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v3...v3.5.3">https://github.com/actions/checkout/compare/v3...v3.5.3</a></p>
<h2>v3.5.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix: Use correct API url / endpoint in GHES by <a
href="https://github.com/fhammerl"><code>@​fhammerl</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1289">actions/checkout#1289</a>
based on <a
href="https://redirect.github.com/actions/checkout/issues/1286">#1286</a>
by <a href="https://github.com/1newsr"><code>@​1newsr</code></a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v3.5.1...v3.5.2">https://github.com/actions/checkout/compare/v3.5.1...v3.5.2</a></p>
<h2>v3.5.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Improve checkout performance on Windows runners by upgrading
<code>@​actions/github</code> dependency by <a
href="https://github.com/BrettDong"><code>@​BrettDong</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1246">actions/checkout#1246</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/BrettDong"><code>@​BrettDong</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1246">actions/checkout#1246</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>v4.2.0</h2>
<ul>
<li>Add Ref and Commit outputs by <a
href="https://github.com/lucacome"><code>@​lucacome</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1180">actions/checkout#1180</a></li>
<li>Dependency updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>- <a
href="https://redirect.github.com/actions/checkout/pull/1777">actions/checkout#1777</a>,
<a
href="https://redirect.github.com/actions/checkout/pull/1872">actions/checkout#1872</a></li>
</ul>
<h2>v4.1.7</h2>
<ul>
<li>Bump the minor-npm-dependencies group across 1 directory with 4
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1739">actions/checkout#1739</a></li>
<li>Bump actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1697">actions/checkout#1697</a></li>
<li>Check out other refs/* by commit by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1774">actions/checkout#1774</a></li>
<li>Pin actions/checkout's own workflows to a known, good, stable
version. by <a href="https://github.com/jww3"><code>@​jww3</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1776">actions/checkout#1776</a></li>
</ul>
<h2>v4.1.6</h2>
<ul>
<li>Check platform to set archive extension appropriately by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1732">actions/checkout#1732</a></li>
</ul>
<h2>v4.1.5</h2>
<ul>
<li>Update NPM dependencies by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1703">actions/checkout#1703</a></li>
<li>Bump github/codeql-action from 2 to 3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1694">actions/checkout#1694</a></li>
<li>Bump actions/setup-node from 1 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1696">actions/checkout#1696</a></li>
<li>Bump actions/upload-artifact from 2 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1695">actions/checkout#1695</a></li>
<li>README: Suggest <code>user.email</code> to be
<code>41898282+github-actions[bot]@users.noreply.github.com</code> by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1707">actions/checkout#1707</a></li>
</ul>
<h2>v4.1.4</h2>
<ul>
<li>Disable <code>extensions.worktreeConfig</code> when disabling
<code>sparse-checkout</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1692">actions/checkout#1692</a></li>
<li>Add dependabot config by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1688">actions/checkout#1688</a></li>
<li>Bump the minor-actions-dependencies group with 2 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1693">actions/checkout#1693</a></li>
<li>Bump word-wrap from 1.2.3 to 1.2.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1643">actions/checkout#1643</a></li>
</ul>
<h2>v4.1.3</h2>
<ul>
<li>Check git version before attempting to disable
<code>sparse-checkout</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1656">actions/checkout#1656</a></li>
<li>Add SSH user parameter by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1685">actions/checkout#1685</a></li>
<li>Update <code>actions/checkout</code> version in
<code>update-main-version.yml</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1650">actions/checkout#1650</a></li>
</ul>
<h2>v4.1.2</h2>
<ul>
<li>Fix: Disable sparse checkout whenever <code>sparse-checkout</code>
option is not present <a
href="https://github.com/dscho"><code>@​dscho</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1598">actions/checkout#1598</a></li>
</ul>
<h2>v4.1.1</h2>
<ul>
<li>Correct link to GitHub Docs by <a
href="https://github.com/peterbe"><code>@​peterbe</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1511">actions/checkout#1511</a></li>
<li>Link to release page from what's new section by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1514">actions/checkout#1514</a></li>
</ul>
<h2>v4.1.0</h2>
<ul>
<li><a href="https://redirect.github.com/actions/checkout/pull/1396">Add
support for partial checkout filters</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/checkout/commit/11bd71901bbe5b1630ceea73d27597364c9af683"><code>11bd719</code></a>
Prepare 4.2.2 Release (<a
href="https://redirect.github.com/actions/checkout/issues/1953">#1953</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/e3d2460bbb42d7710191569f88069044cfb9d8cf"><code>e3d2460</code></a>
Expand unit test coverage (<a
href="https://redirect.github.com/actions/checkout/issues/1946">#1946</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/163217dfcd28294438ea1c1c149cfaf66eec283e"><code>163217d</code></a>
<code>url-helper.ts</code> now leverages well-known environment
variables. (<a
href="https://redirect.github.com/actions/checkout/issues/1941">#1941</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871"><code>eef6144</code></a>
Prepare 4.2.1 release (<a
href="https://redirect.github.com/actions/checkout/issues/1925">#1925</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/6b42224f41ee5dfe5395e27c8b2746f1f9955030"><code>6b42224</code></a>
Add workflow file for publishing releases to immutable action package
(<a
href="https://redirect.github.com/actions/checkout/issues/1919">#1919</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/de5a000abf73b6f4965bd1bcdf8f8d94a56ea815"><code>de5a000</code></a>
Check out other refs/* by commit if provided, fall back to ref (<a
href="https://redirect.github.com/actions/checkout/issues/1924">#1924</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/d632683dd7b4114ad314bca15554477dd762a938"><code>d632683</code></a>
Prepare 4.2.0 release (<a
href="https://redirect.github.com/actions/checkout/issues/1878">#1878</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/6d193bf28034eafb982f37bd894289fe649468fc"><code>6d193bf</code></a>
Bump braces from 3.0.2 to 3.0.3 (<a
href="https://redirect.github.com/actions/checkout/issues/1777">#1777</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/db0cee9a514becbbd4a101a5fbbbf47865ee316c"><code>db0cee9</code></a>
Bump the minor-npm-dependencies group across 1 directory with 4 updates
(<a
href="https://redirect.github.com/actions/checkout/issues/1872">#1872</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/b6849436894e144dbce29d7d7fda2ae3bf9d8365"><code>b684943</code></a>
Add Ref and Commit outputs (<a
href="https://redirect.github.com/actions/checkout/issues/1180">#1180</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/checkout/compare/v3...v4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-24 07:38:03 -04:00
dependabot[bot] fba2f135a3 Update sphinx requirement from <6,>=5 to >=5,<9 (#542)
Updates the requirements on
[sphinx](https://github.com/sphinx-doc/sphinx) to permit the latest
version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/sphinx-doc/sphinx/releases">sphinx's
releases</a>.</em></p>
<blockquote>
<h2>Sphinx 8.1.3</h2>
<p>Changelog: <a
href="https://www.sphinx-doc.org/en/master/changes/8.1.html">https://www.sphinx-doc.org/en/master/changes/8.1.html</a></p>
<h2>Bugs fixed</h2>
<ul>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/13013">#13013</a>:
Restore support for <code>cut_lines()</code> with no object type. Patch
by Adam Turner.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/sphinx-doc/sphinx/blob/v8.1.3/CHANGES.rst">sphinx's
changelog</a>.</em></p>
<blockquote>
<h1>Release 8.1.3 (released Oct 13, 2024)</h1>
<h2>Bugs fixed</h2>
<ul>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/13013">#13013</a>:
Restore support for :func:<code>!cut_lines</code> with no object type.
Patch by Adam Turner.</li>
</ul>
<h1>Release 8.1.2 (released Oct 12, 2024)</h1>
<h2>Bugs fixed</h2>
<ul>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/13012">#13012</a>:
Expose :exc:<code>sphinx.errors.ExtensionError</code> in
<code>sphinx.util</code>
for backwards compatibility.
This will be removed in Sphinx 9, as exposing the exception
in <code>sphinx.util</code> was never intentional.
:exc:<code>!ExtensionError</code> has been part of
<code>sphinx.errors</code> since Sphinx 0.9.
Patch by Adam Turner.</li>
</ul>
<h1>Release 8.1.1 (released Oct 11, 2024)</h1>
<h2>Bugs fixed</h2>
<ul>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/13006">#13006</a>:
Use the preferred <a
href="https://www.cve.org/">https://www.cve.org/</a> URL for
the :rst:role:<code>:cve: &lt;cve&gt;</code> role.
Patch by Hugo van Kemenade.</li>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/13007">#13007</a>:
LaTeX: Improve resiliency when the required
<code>fontawesome</code> or <code>fontawesome5</code> packages are not
installed.
Patch by Jean-François B.</li>
</ul>
<h1>Release 8.1.0 (released Oct 10, 2024)</h1>
<h2>Dependencies</h2>
<ul>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/12756">#12756</a>:
Add lower-bounds to the <code>sphinxcontrib-*</code> dependencies.
Patch by Adam Turner.</li>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/12833">#12833</a>:
Update the LaTeX <code>parskip</code> package from 2001 to 2018.
Patch by Jean-François B.</li>
</ul>
<h2>Incompatible changes</h2>
<ul>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/12763">#12763</a>:
Remove unused internal class <code>sphinx.util.Tee</code>.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/a1510de4777eaa2e569435f95b05f6f3293d7035"><code>a1510de</code></a>
Bump to 8.1.3 final</li>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/62e9606d63c8bbb4964213fd6b427d1483847662"><code>62e9606</code></a>
Restore support for <code>cut_lines()</code> with no object type (<a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/13015">#13015</a>)</li>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/5ae32ce9bfe4a17a7f00e1e8d39a80449423c726"><code>5ae32ce</code></a>
Bump version</li>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/a72b47bb408923cb7809eb9f96885545184e3773"><code>a72b47b</code></a>
Bump to 8.1.2 final</li>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/39a45ad4073a4d8c3b7dfd64d22e8a88870dcc7c"><code>39a45ad</code></a>
Expose <code>ExtensionError</code> in <code>sphinx.util</code> for
backwards compatibility.</li>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/5a4859a2e489c66b38804e95bf77fd0baf4320dc"><code>5a4859a</code></a>
Add docs about sphinx-autobuild (<a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/13011">#13011</a>)</li>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/05679efe7b34f8b2fb87605438c40248ac8cae83"><code>05679ef</code></a>
Type-check the 'autodoc_intenum' example (<a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/12827">#12827</a>)</li>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/86d1d31fb370f031739079de7d827be0074e7661"><code>86d1d31</code></a>
Prune CHANGES of unneeded sections</li>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/b6269d3790bb3bdd652ce67fecb59e6afddc8014"><code>b6269d3</code></a>
Improve documentation for the Builder API (<a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/13008">#13008</a>)</li>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/c46abc47210088a6c4fee9dac23badfcebc441d7"><code>c46abc4</code></a>
Improve clarity for <code>master_doc</code> and
<code>root_doc</code></li>
<li>Additional commits viewable in <a
href="https://github.com/sphinx-doc/sphinx/compare/v5.0.0...v8.1.3">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-24 07:37:38 -04:00
Andreas Motl 4006de72cd Dependencies: Add Dependabot configuration (#534)
What the title says.
2024-10-24 07:30:41 -04:00
Andreas Motl b3c7252197 Chore: Format code using Ruff, and fix linter errors (#531)
## About
- Add Ruff configuration to `pyproject.toml`, apply its formatter, and
satisfy its linter.
- Migrate pytest configuration to `pyproject.toml`.
2024-10-24 07:30:18 -04:00
Andreas Motl 398ac3343e This and that: 20241024-02 (#530)
## About
After GH-529, another round of improvements submitted as a bundle.

## References
- GH-529
2024-10-23 21:04:03 -04:00
Andreas Motl 8b197ba361 CI: Improve test matrix configuration. Add macOS, both Intel and ARM. 2024-10-24 02:27:32 +02:00
Andreas Motl e700aa2937 Chore: Update LICENSE file
GitHub wasn't able to discover the license (badge) from the LICENSE
file. Let's use a vanilla variant for Apache 2.0.
2024-10-24 02:27:32 +02:00
Andreas Motl 3894550642 Tests: Enable pytest options, increasing verbosity
That is to display test case names, not just dots that don't convey
much.
2024-10-24 02:27:32 +02:00
Andreas Motl 43fd041138 CI: Add PyPy to Python test matrix 2024-10-24 02:27:32 +02:00
Andreas Motl 363af5338d CI: Properly verify package on Python 3.10, 3.11, and 3.12 2024-10-24 02:27:32 +02:00
Andreas Motl 55430a4366 Dependencies: Separate runtime vs. test vs. development definitions 2024-10-24 02:27:32 +02:00
kennethreitz f7c6a3ae97 Docs: Update to jinja2<3.2, Dependabot admonitions versions <3.1.4 (#528)
## About
What the title says.

## Details
It's only about building static docs, so there is no danger for this
project. It's just a chore fix to properly dismiss the security warning
signalled by Dependabot.

## References
- https://github.com/kennethreitz/responder/security/dependabot/61
2024-10-23 19:38:02 -04:00
Andreas Motl dcadba1425 Docs: Update to jinja2<3.2, Dependabot admonitions versions <3.1.4 2024-10-24 01:36:07 +02:00
kennethreitz de08b15ae8 Docs: Minimally modernize Sphinx configuration. Fix building on Python 3.11. (#526)
## About
Just a little maintenance patch for the Sphinx docs, to minimally
modernize dependencies, and to fix the build [^1].

[^1]: [...] and to probe if any commit styles of mine (commit messages,
wording, whatever) need to be adjusted to comply with any policies
employed here.
2024-10-23 19:32:44 -04:00
kennethreitz 0cfca6d906 Merge branch 'main' into docs-dependencies 2024-10-23 19:30:17 -04:00
kennethreitz a73e413a66 CI: Slightly update GHA configuration, now targeting branch main (#527)
## Problem
CI did not start on GH-526.

## Details
Also, add a configuration snippet to cancel redundant in-progress jobs,
in order to save resources. That means running jobs are terminated when
subsequently pushing to the same branch / updating the same PR,
DWIM-like.
2024-10-23 19:30:01 -04:00
Andreas Motl 87931a25d0 CI: Slightly update GHA configuration, now targeting branch main
Also, add a configuration snippet to cancel redundant in-progress jobs.
That means running jobs are terminated when subsequently pushing to the
same branch, in order to save resources.
2024-10-24 01:24:57 +02:00
Andreas Motl 1fd9a682dd Docs: Fix broken links 2024-10-24 01:17:54 +02:00
Andreas Motl 5d3e650901 Docs: Update dependencies, fixing the build on Python 3.11 2024-10-24 01:13:22 +02:00
Andreas Motl 48d082e6a5 Docs: Use relaxed upper-bound dependency pinning for Sphinx dependencies 2024-10-24 01:04:38 +02:00
Andreas Motl 87e22481e8 Docs: Clean up docs/requirements.txt. It just needs Sphinx and friends. 2024-10-24 01:04:08 +02:00
Andreas Motl e48ce6c301 Chore: Update .gitignore to ignore all virtualenvs 2024-10-24 01:02:25 +02:00
kennethreitz e9613500da Delete Pipfile (#516) 2024-03-31 10:56:22 -04:00
kennethreitz c2943accd0 Delete Pipfile 2024-03-31 10:54:49 -04:00
kennethreitz 649a255657 remove files 2024-03-30 20:43:28 -04:00
kennethreitz 7eaaaaafe1 Add Flask to requirements.txt 2024-03-30 20:37:52 -04:00
kennethreitz ae09b88978 Add typesystem==0.2.5 to requirements.txt 2024-03-30 20:37:05 -04:00
kennethreitz e3e307fd68 Update uv pip install command to use --system flag 2024-03-30 20:36:10 -04:00
kennethreitz 89f0724029 Update test.yaml workflow 2024-03-30 20:31:20 -04:00
kennethreitz bebe62adaf Add apistar to requirements.txt 2024-03-30 20:28:08 -04:00
kennethreitz eb9cddc8c2 Update dependency installation command 2024-03-30 20:27:30 -04:00
kennethreitz 7c19eca78a Remove unused code and dependencies 2024-03-30 20:26:43 -04:00
kennethreitz ed28b11d21 remove schema_doc.py 2024-03-30 20:21:38 -04:00
kennethreitz 46cdd4a245 Update GraphQL dependencies 2024-03-30 20:18:42 -04:00
kennethreitz ac91b172e6 Add graphql_server to requirements.txt 2024-03-30 20:15:50 -04:00
kennethreitz ed0da6d462 test 2024-03-30 20:14:26 -04:00
kennethreitz 555e9bff65 Add helloworld.py and update serve method in api.py 2024-03-30 20:11:42 -04:00
kennethreitz bf43d9f202 Add graphene to required packages 2024-03-30 20:08:25 -04:00
kennethreitz e239cc304d Update Python versions in test.yaml 2024-03-30 20:06:05 -04:00
kennethreitz 3285bd57c7 Update python_requires to >=3.11 2024-03-30 20:05:53 -04:00
kennethreitz 3090fb9e68 Update branch name in GitHub Actions workflow 2024-03-30 20:03:52 -04:00
kennethreitz e90bd24ebe Update test.yaml, add Pipfile, and delete httpbin.py 2024-03-30 20:02:39 -04:00
kennethreitz a0acc03a97 delete lint 2024-03-30 19:56:14 -04:00
kennethreitz 8a668e6efe Update GitHub Actions workflow for Python testing 2024-03-30 19:55:56 -04:00
kennethreitz 4c75742e4d Update Python versions and operating systems in test.yaml 2024-03-30 19:54:18 -04:00
kennethreitz 796fdc2ddf Fix commented out code in test_responder.py 2024-03-30 19:48:57 -04:00
kennethreitz a8caa3054b Update API requests from GET to POST 2024-03-30 19:44:45 -04:00
kennethreitz 2ef9e133ad Remove unused dependencies and update setup.py 2024-03-30 19:37:46 -04:00
kennethreitz 2ec570ad61 Refactor code by removing unused imports and properties 2024-03-30 19:35:12 -04:00
taoufik07 02aa338970 v2.0.7 2021-01-08 09:27:53 +01:00
Taoufik 882250bd86 Merge pull request #450 from taoufik07/uvicron_extra_standard
Add uvicorn[extra]
2021-01-08 09:26:31 +01:00
taoufik07 3809eda2f2 Use uvicorn[standard] 2021-01-07 21:39:17 +01:00
Taoufik b32eda70d2 Merge pull request #448 from taoufik07/taoufik07-patch-1
Add test for 3.9
2021-01-06 09:05:23 +01:00
taoufik07 f1b2f46a10 v2.0.6 2021-01-06 09:02:42 +01:00
Taoufik cf82dac4ad Add test for 3.9 2021-01-06 08:58:58 +01:00
Taoufik a0913e3f63 Merge pull request #447 from timgates42/bugfix_typo_marshmallow
docs: fix simple typo, mashmallow -> marshmallow
2020-12-24 08:44:43 +01:00
Tim Gates f90955a9b9 docs: fix simple typo, mashmallow -> marshmallow
There is a small typo in responder/ext/schema/__init__.py.

Should read `marshmallow` rather than `mashmallow`.
2020-12-24 13:27:01 +11:00
Taoufik 3736c9229d Merge pull request #429 from taoufik07/dependabot/pip/docs/bleach-3.1.4
Bump bleach from 3.1.1 to 3.1.4 in /docs
2020-12-02 09:44:55 +01:00
dependabot[bot] a802853367 Bump bleach from 3.1.1 to 3.1.4 in /docs
Bumps [bleach](https://github.com/mozilla/bleach) from 3.1.1 to 3.1.4.
- [Release notes](https://github.com/mozilla/bleach/releases)
- [Changelog](https://github.com/mozilla/bleach/blob/master/CHANGES)
- [Commits](https://github.com/mozilla/bleach/compare/v3.1.1...v3.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-01 23:11:49 +00:00
Taoufik 96ca88fe88 Merge pull request #446 from taoufik07/github_actions
Switch to github actions
2020-12-02 00:11:05 +01:00
taoufik07 a57570210a Add prettier to pre-commit 2020-12-01 23:36:42 +01:00
taoufik07 7682e94b35 Disable tests in CI for windows 2020-12-01 23:10:24 +01:00
taoufik07 8bbebe113c Bump black 2020-12-01 23:10:24 +01:00
taoufik07 7c921f827b Switch to github actions 2020-12-01 23:10:24 +01:00
taoufik07 4cc055f93a Add pre-commit 2020-12-01 23:10:21 +01:00
Taoufik e596a8b457 Merge pull request #444 from majiang/patch-1
Call user-provided `default_response`
2020-12-01 21:42:13 +01:00
Taoufik fd2da55880 Merge pull request #445 from majiang/patch-2
test_redirects: access '/2' and redirect to '/1'
2020-12-01 21:37:28 +01:00
majiang 975e9b5643 test_redirects: access '/2' and redirect to '/1' 2020-12-01 16:18:56 +09:00
majiang c0036e0474 Call user-provided default_response
I'm not 100% sure, but it seems that user-provided `default_response`, stored as `Router.default_endpoint`, should be called when no match was found.
2020-12-01 16:15:57 +09:00
Taoufik 103816e27a Merge pull request #439 from ryuuji/master
bump uvicorn 0.11.* to 0.11.7
2020-08-12 11:01:13 +02:00
Ryuuji Yoshimoto b7c1684ab4 bump 0.11.7 2020-08-11 18:57:35 +09:00
Ryuuji Yoshimoto 16bd6ca266 bump uvicorn 2020-08-11 18:53:59 +09:00
Taoufik 20bae4712b Merge pull request #430 from ucpr/fix-lock
Fix hash in pygment from piwheel to pypi.
2020-04-17 05:40:29 +02:00
ucpr a7aa80c690 Fix hash in pygment from piwheel to pypi. 2020-04-15 00:30:34 +09:00
Taoufik df89d1d58b Merge pull request #424 from taoufik07/starlette_0_13
Fixes and bump dependencies
2020-03-09 05:32:30 +01:00
taoufik07 477cddd29c travisci add python 3.8 2020-03-09 05:26:48 +01:00
taoufik07 9b8cf3a1b1 Bump uvicorn version to 0.11 2020-03-09 05:16:34 +01:00
taoufik07 2871a3c07f starlette 0.13 and fix lifespan 2020-03-09 05:13:12 +01:00
Taoufik 13763296dd Merge pull request #421 from taoufik07/dependabot/pip/docs/bleach-3.1.1
Bump bleach from 3.0.2 to 3.1.1 in /docs
2020-02-25 02:24:11 +01:00
dependabot[bot] 783b22ab1c Bump bleach from 3.0.2 to 3.1.1 in /docs
Bumps [bleach](https://github.com/mozilla/bleach) from 3.0.2 to 3.1.1.
- [Release notes](https://github.com/mozilla/bleach/releases)
- [Changelog](https://github.com/mozilla/bleach/blob/master/CHANGES)
- [Commits](https://github.com/mozilla/bleach/compare/v3.0.2...v3.1.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-02-24 18:31:05 +00:00
Taoufik 109937adf4 Merge pull request #414 from taoufik07/v2.0.5
v2.0.5
2019-12-15 16:38:02 +01:00
taoufik 63ea9cc4e0 v2.0.5 2019-12-15 16:32:47 +01:00
Taoufik ec40a0c4c3 Merge pull request #413 from taoufik07/support_python3.8
Update requirements to support python3.8
2019-12-15 16:28:17 +01:00
taoufik 0855d1a378 Update requirements to support python3.8 2019-12-15 16:22:26 +01:00
Taoufik 77fe17d350 Merge pull request #411 from StevenAvelino24/feature/use-openapi-params
Use OpenAPI info params on init of API
2019-12-04 20:07:46 +01:00
Steven Avelino 0b8a031ccb Use openapi info params on init of API 2019-12-04 14:50:30 +01:00
taoufik 0678daa880 Bump to v2.0.4 2019-11-19 17:35:16 +01:00
Taoufik 6761e3bdd8 Merge pull request #406 from daphil19/master
fix issues related to using `static=true` in `api.add_route()` and `static_route` in `responder.API()`
2019-11-19 17:30:22 +01:00
David Phillips ead213a506 responder now checks routes added via add_route before mounted routes
routes added by `add_route` (including @api.route()) are now checked
before routes that were added by api.mount()

this especially helps cases for situations where the static route
defined by the API class is '/' but the default route is '/' as well
2019-11-02 13:57:39 -04:00
David Phillips 75b5782eee fix static_response endpoint to return index.html contents 2019-11-02 13:55:34 -04:00
Taoufik a80df809e4 Merge pull request #399 from vbarbaresi/fix_render_async
Async templates require enable_async in jinja2 Environment
2019-10-25 22:19:04 +01:00
taoufik 7f3177f662 v2.0.3 2019-10-22 10:41:34 +01:00
taoufik 906cd2fbbf Fix templates conflicts 2019-10-20 12:48:02 +01:00
taoufik 9d0129da56 Fix templates conflicts 2019-10-20 12:39:32 +01:00
taoufik aedcf12d99 v2.0.1 2019-10-20 12:20:54 +01:00
Vincent Barbaresi 86361523e2 fix async templates rendering requiring enable_async in jinja2
When trying to test the render_async() feature I realized that it wasn't working
The template instance needs to be created with enable_async=True
2019-10-19 14:49:50 +02:00
Taoufik a7110ef441 Merge pull request #395 from vbarbaresi/master
add a few tests for API and remove some dead code
2019-10-19 13:06:01 +01:00
Vincent Barbaresi d3e4968546 add a few tests for API and remove some dead code
Increasing the coverage of api.py to 86%

- use pytest.parametrize to test cors and hsts parameters usage
- use pytest.parametrize to factorize test_staticfiles_custom_route
- add tests for path_matches_route and Route() with no endpoint
- test Jinja template rendering from a file

- remove   _notfound_wsgi_app, before_ws_requests, before_http_requests

they didn't seem used anywhere and the before_* method were broken, referring to
self.before_requests that doesn't exist anymore
2019-10-19 13:47:39 +02:00
Taoufik 03e34d56ab Merge pull request #398 from taoufik07/v2
v2.0.0
2019-10-19 12:22:32 +01:00
taoufik b470d10416 v2.0.0 2019-10-19 12:21:06 +01:00
Taoufik b8aea89039 Merge pull request #397 from taoufik07/update_pipfile
Bump versions
2019-10-19 12:13:06 +01:00
taoufik 4a1e89af1b Bump versions 2019-10-19 12:07:31 +01:00
Taoufik 81fbc94d36 Merge pull request #396 from vbarbaresi/fix_3.8_tests
upgrade pluggy to 0.13.0 to support importlib.metadata on python 3.8
2019-10-03 17:24:53 +01:00
Vincent Barbaresi 6487671559 upgrade pluggy to 0.13.0 to support stb lib importlib.metadata on python 3.8 2019-10-03 18:15:09 +02:00
Taoufik 838c7f29b5 Update docs url 2019-09-26 12:07:59 +01:00
Taoufik df85a4c214 Merge pull request #388 from taoufik07/new-router
New Router and refactor
2019-09-03 20:13:46 +02:00
taoufik 1bdbea238e Add a api.static_app and extend starlette's StaticFiles app 2019-08-31 01:38:00 +02:00
Taoufik 97dbef92d9 Bump werkzug version to 0.15.5 in docs 2019-08-21 23:05:27 +02:00
taoufik 85c1c0036c [WIP] Update docs 2019-08-21 22:50:58 +02:00
taoufik 0653ee2c6b Add Templates 2019-08-21 13:46:31 +02:00
taoufik d1db913c7d Bump dependencies 2019-08-17 22:15:43 +02:00
taoufik d24b921cdc Move open api schema to ext/schema 2019-08-17 18:25:19 +02:00
taoufik b31b742787 Implement a new Router and other changes
- Router
- Use starlette's Session middlewre
- Add Exception Middleware
- ...
2019-08-17 14:41:11 +02:00
Taoufik d820f0277f Merge pull request #387 from taoufik07/v-1.3.2
Bump version and update CHANGELOG
2019-08-15 22:57:21 +02:00
taoufik 7219856177 Bump version and update CHANGELOG 2019-08-15 22:51:28 +02:00
Taoufik e6d302aabb Merge pull request #385 from FirstKlaas/state-for-request
Added a state property to the request object.
2019-08-07 19:28:13 +02:00
FirstKlaas d73243ab60 Linting with black 2019-08-07 18:44:43 +02:00
FirstKlaas 8101e7d7b0 Added a state property to the request opbject. 2019-08-07 18:29:03 +02:00
Taoufik 784c7e72ae Merge pull request #381 from taoufik07/ci-py-3.8-dev
Travis-ci add python 3.8-dev
2019-08-05 20:08:44 +02:00
taoufik 93156fd2f1 Travis-ci add python 3.8-dev 2019-08-05 20:00:14 +02:00
Taoufik e4f6898498 Merge pull request #380 from taoufik07/refactor-changelog
Refactor CHANGELOG following the keepachangelog format
2019-08-05 19:59:00 +02:00
taoufik ef330135f9 Refactor CHANGELOG following the keepachangelog format
- Format following https://keepachangelog.com/en/1.0.0/
- Refactor CHANGELOGS
- Add missing changes
2019-08-05 19:56:01 +02:00
Taoufik 54cbbdface Merge pull request #379 from taoufik07/asgi3
ASGI 3 support
2019-08-05 19:38:46 +02:00
taoufik f3c9320837 ASGI 3 support 2019-08-05 15:33:56 +02:00
Taoufik 0529629ac8 Merge pull request #378 from ucpr/add_formats_test
add test for #367
2019-07-29 23:00:50 +02:00
Taoufik 22af42ead6 Merge pull request #377 from ucpr/fix_readme
fix links
2019-07-29 22:59:40 +02:00
ucpr bd2efb68e1 test: add test for #367 2019-07-30 00:26:01 +09:00
ucpr 37ba3d2efc fix: fix links 2019-07-29 23:24:57 +09:00
kennethreitz ed8afeaa87 Merge pull request #367 from ucpr/fix_format_form
format data when mimetype is form-data
2019-07-16 11:05:57 -04:00
kennethreitz ee6efe5aa4 Merge pull request #372 from mlschneid/master
Encouraging use of stable
2019-07-16 11:05:07 -04:00
Mike Schneider 3a8113d8b0 Found more --pre flags 2019-07-16 07:43:55 -07:00
Mike Schneider 7afce42943 Encouraging use of stable 2019-07-16 07:34:35 -07:00
Frost Ming 67a6c25256 Merge pull request #370 from waghanza/remove_pipenv_requires
Specify constraint for python version in pipenv
2019-07-09 17:15:53 +08:00
Marwan Rabbâa 38dea8311c refactor(pip): Remove pipenv restriction on python version 2019-07-09 11:09:29 +02:00
Marwan Rabbâa 555e1f7924 refactor(pip): Specify constraint for python version in pipenv 2019-07-08 18:35:42 +02:00
Taoufik 8b87f63609 Merge pull request #369 from waghanza/py_37
Add python3.7 on CI
2019-07-08 13:56:46 +02:00
Marwan Rabbâa f01b1d493f feat(ci): Test under python 3.7 2019-07-08 09:26:34 +02:00
ucpr a802245bf0 format data when mimetype is form-data 2019-07-01 05:58:43 +09:00
kennethreitz 6dbbad158a Merge pull request #357 from amikrop/master
Remove extra `static_url` function in `API.docs`
2019-06-25 09:17:14 -07:00
kennethreitz 877fe144b4 Merge pull request #365 from tedder/ted/add_links_2
add some links to the main page
2019-06-25 09:16:47 -07:00
ted 70e6bc0466 add some links to the main page
Basically linked things I had to google/look up on my own. There are a million things that _could_ be linked, but using myself as a median Python coder, it seems about right. Removed italics because I can't figure out how to use them with a link.
2019-06-25 08:21:07 -07:00
Taoufik 63f2e833eb Merge pull request #362 from mtcronin99/patch-1
Update tour.rst to add quotes around hostnames
2019-06-04 17:29:35 +02:00
mtcronin99 fb71abe534 Update tour.rst to add quotes around hostnames
The API call in the Trusted Hosts section needs quotes around the hostnames in the example.
2019-06-04 11:21:27 -04:00
Taoufik 8ccb39560e Merge pull request #359 from EdwardBetts/patch-1
Correct a spelling mistake
2019-05-11 22:24:16 +02:00
Edward Betts e6b880be62 Correct a spelling mistake 2019-05-11 20:17:10 +01:00
kennethreitz d0016ac7c9 Update README.md 2019-05-06 07:42:31 -04:00
Aristotelis Mikropoulos 05035e0171 Remove extra static_url function in API.docs 2019-05-02 17:09:34 +03:00
Taoufik 78b5bef879 Merge pull request #354 from kennethreitz/starlette-new-lifespan
Starlette 0.11.* and cleanup
2019-04-29 23:14:27 +02:00
taoufik a6955b5db5 Fix lifespan and bump starlette to 0.11.* 2019-04-29 23:09:48 +02:00
taoufik a1a0a1b71e Remove duplicated code and rename _dispatch_request 2019-04-29 22:44:44 +02:00
Taoufik 0bdde6d5fe Merge pull request #353 from kennethreitz/taoufik07-patch-1
Typo schema docs
2019-04-28 16:02:29 +02:00
Taoufik cf5447d5bd Typo schema docs 2019-04-28 15:59:15 +02:00
taoufik b2dd2c205d Fix tests 2019-04-28 15:48:36 +02:00
kennethreitz e52c9277c8 fix lockfile 2019-04-28 09:43:29 -04:00
kennethreitz 712ec2410d changes 2019-04-28 09:39:12 -04:00
kennethreitz dea2ca41d2 Merge pull request #351 from kennethreitz/route-params-docs
Add docs for route params convertors
2019-04-28 09:19:42 -04:00
Taoufik ca0f32c02b Black check before tests 2019-04-27 20:44:02 +02:00
Taoufik f21b296fba Add docs for route params convertors 2019-04-27 20:10:31 +02:00
Taoufik 3224479b99 Merge pull request #350 from kennethreitz/cleanup
Cleanup
2019-04-27 13:52:48 +02:00
Taoufik f95950eedc Cleanup 2019-04-27 13:49:54 +02:00
kennethreitz 4467376d0a Merge pull request #333 from taoufik07/custom_specifiers
Route params convertors
2019-04-27 07:32:13 -04:00
kennethreitz ac65dc5361 Merge pull request #347 from s-pace/doc/add_search
[doc website] Add a nice search experience
2019-04-27 07:25:38 -04:00
s-pace 4957793c80 feat: add search to every documentation pages 2019-04-22 22:23:07 +02:00
s-pace ff7f4b502d feat: add search to the main introduction page 2019-04-22 22:22:46 +02:00
Timo Furrer 816cb7188b Merge pull request #339 from jonbeebe/master
Removed asgiref dependency
2019-03-29 18:34:14 +01:00
Jonathan Beebe 6456d435eb Revert Pipfile.lock (with asgiref removed) 2019-03-28 20:36:11 -07:00
Jonathan Beebe 63e338ed6f Removed asgiref dependency
Replaced WsgiToAsgi (asgiref.wsgi) usage with WSGIMiddleware (starlette.middleware.wsgi) to fix breaking changes with asgiref 3.0.0.
2019-03-28 20:12:22 -07:00
Timo Furrer 00211c8f03 Merge pull request #335 from kobayashi/master
add sample of uploading a file
2019-03-21 21:30:14 +01:00
kobayashi ebed9fe3aa fix typos 2019-03-17 19:05:14 -04:00
kobayashi 734b5e7303 add sample of uploading a file 2019-03-17 15:21:27 -04:00
Taoufik 1696d501e2 Merge pull request #334 from MerleLiuKun/fix_test_responder_variable
fix variable error at test responder
2019-03-14 08:19:53 +00:00
ikronskun e65d2f8c50 fix variable error at test responder 2019-03-14 14:25:45 +08:00
taoufik07 9ea705b2ea Specifiers test 2019-03-12 17:39:53 +01:00
taoufik07 5a5a811dca Add routes specifiers 2019-03-12 16:58:36 +01:00
kennethreitz df7b9419c2 Merge pull request #332 from jlewis91/patch-1
Update tour.rst to be openapi compliant
2019-03-10 10:55:38 -04:00
Jeremiah 37318f1106 Update tour.rst 2019-03-10 14:53:44 +01:00
kennethreitz 19e9f6ac5d Merge pull request #320 from taoufik07/static_serve
Fix #303
2019-03-05 08:33:39 -05:00
kennethreitz 658b51a449 Merge pull request #321 from taoufik07/revert_before_requests
Revert before_requests
2019-03-05 08:33:25 -05:00
Parth Shandilya 485303c0f2 Merge pull request #314 from vlcinsky/fix_uvloop_env_marker
Fix #313 incomplete environment marker for uvloop
2019-03-03 23:22:53 +05:30
taoufik07 885d902b7d Revert 2019-02-26 23:46:07 +01:00
taoufik07 a35f02fb64 Add tests 2019-02-26 22:23:43 +01:00
taoufik07 28d1f16ad5 Disable serving when static_dir is None and handle templates_dir 2019-02-26 22:10:13 +01:00
Taoufik a04d7c3a9a Merge pull request #319 from taoufik07/refactor_before_ws
Refactor before_requests and websockets
2019-02-26 19:27:26 +00:00
taoufik07 b876f8484c Update docs 2019-02-26 17:01:13 +01:00
taoufik07 854c6d3d65 Add @before_request 2019-02-26 16:44:12 +01:00
taoufik07 f9a850a8fe Add before_requets for ws and refactor 2019-02-26 15:50:55 +01:00
taoufik07 e808662fe7 Refactor websockets 2019-02-26 15:27:26 +01:00
Taoufik 7bbb02126e Merge pull request #316 from tkamenoko/patch-4
fix typo
2019-02-23 15:15:31 +00:00
Taoufik aa101059a7 Merge pull request #315 from taoufik07/websockets_tests
Websockets tests
2019-02-23 15:11:23 +00:00
T.Kameyama d1f7fe02e4 fix typo 2019-02-24 00:10:47 +09:00
taoufik07 3e26dc1373 Add websockets tests 2019-02-23 16:00:22 +01:00
Jan Vlčinský 0a9d819555 Merge branch 'master' into fix_uvloop_env_marker 2019-02-22 20:47:25 +01:00
Jan Vlcinsky b31dfeefb7 Fix #313 incomplete environment marker for uvloop 2019-02-22 20:44:32 +01:00
Taoufik fc640ec331 Merge pull request #312 from iancleary/bug/242_docs_consistency
fixed flow between OpenAPI and Interactive Docs sections
2019-02-22 13:33:37 +00:00
iancleary 3382723457 fixed flow between OpenAPI and Interactive Docs sections 2019-02-22 06:16:45 -07:00
Taoufik 1fc0722ad6 Merge pull request #310 from taoufik07/fix/req_text
DEFAULT_ENCODING if none is detected
2019-02-22 11:57:11 +00:00
taoufik07 b21e308357 Add tests 2019-02-22 12:44:32 +01:00
taoufik07 738105314b Return DEFAULT_ENCODING if none and remove redundant code 2019-02-22 12:34:22 +01:00
Timo Furrer f3cdc99b29 release: 1.3.0 2019-02-22 10:36:57 +00:00
Taoufik eb70376438 v1.3.0 changelog 2019-02-22 10:34:19 +00:00
Timo Furrer dae1a4fa35 Add template for 1.3.0 CHANGELOG 2019-02-22 10:16:17 +00:00
Taoufik 2ad351197e Merge pull request #304 from taoufik07/content_type
Add resp.html property and make resp.text a property
2019-02-22 10:12:46 +00:00
Taoufik 3d9235c4bc Merge pull request #293 from taoufik07/multiple_cookies_and_directives
Multiple cookies and directives
2019-02-22 10:12:18 +00:00
taoufik07 2cd5596def lint 2019-02-22 10:40:27 +01:00
Taoufik d4191030d9 Merge branch 'master' into multiple_cookies_and_directives 2019-02-22 10:33:53 +01:00
Taoufik 447630a051 Merge branch 'master' into content_type 2019-02-22 10:30:38 +01:00
Timo Furrer f7b53a4895 Merge pull request #308 from taoufik07/feature/stream
Support stream response
2019-02-22 10:15:42 +01:00
taoufik07 21896aa171 Support stream response 2019-02-22 03:58:09 +01:00
Taoufik e8a15697d2 Merge pull request #306 from iancleary/242_modify_swagger_strings
implemented rest of OpenAPI Info Object
2019-02-22 02:05:23 +00:00
icleary 0030993631 api OpenAPI params match /docs display order, updated tour docs and docs test 2019-02-21 18:35:19 -07:00
taoufik07 13ba2f72f5 Update docs 2019-02-22 02:00:12 +01:00
taoufik07 9f39917895 Update docs and README 2019-02-22 01:12:25 +01:00
taoufik07 1b0859fdbb Only encode text 2019-02-22 00:59:17 +01:00
taoufik07 acd1561b1b Add tests 2019-02-22 00:01:49 +01:00
icleary 9f2182949d snake case for terms_of_service, is not None for if statements 2019-02-21 08:35:53 -07:00
icleary 6e5b3a4bf9 ran black on changed file 2019-02-20 22:38:57 -07:00
icleary d2ec323888 edited docstring to remove ->type 2019-02-20 22:29:49 -07:00
icleary 8b9645cf2d implemented rest of OpenAPI Info Object 2019-02-20 22:04:58 -07:00
Taoufik 4ecfef0ddf Merge branch 'master' into content_type 2019-02-21 02:57:06 +01:00
taoufik07 84fb7bd622 resp.html 2019-02-21 02:55:20 +01:00
taoufik07 0b261252e1 Add resp.html property and make resp.text a property 2019-02-21 02:49:47 +01:00
Taoufik d60b5ee39e Merge pull request #297 from taoufik07/whitnoise_notfound 2019-02-21 00:19:02 +00:00
taoufik07 e2f887ec5f Add tests 2019-02-21 00:51:31 +01:00
taoufik07 97da6a6694 Format static_route 2019-02-21 00:46:27 +01:00
taoufik07 c0e9a6778d Set static_response status if not found 2019-02-20 23:20:37 +01:00
Taoufik 5c327a2e0b Merge pull request #302 from taoufik07/lock_update
Pin starlette to 0.10.* and update the lock file
2019-02-20 17:52:00 +00:00
taoufik07 5ed45634cb Pin starlette to 0.10.* and update the lock file 2019-02-20 18:45:53 +01:00
Taoufik a50a373e84 Merge pull request #301 from taoufik07/starlette-0.10.5 2019-02-19 21:59:27 +00:00
taoufik07 86705d0c2f Pin starlette to 0.10.5 2019-02-19 22:35:23 +01:00
taoufik07 b9581444f9 Return 404 when static file is not found 2019-02-19 14:47:31 +01:00
Taoufik 2a60b094b8 Merge branch 'master' into multiple_cookies_and_directives 2019-02-19 13:49:21 +01:00
taoufik07 1ec567cabf Cleanup and black 2019-02-19 13:47:59 +01:00
Taoufik 4fd898b239 Merge pull request #296 from taoufik07/travis_black_check 2019-02-19 12:43:01 +00:00
taoufik07 03d6b72a00 Add linting checks to travis 2019-02-19 13:34:02 +01:00
taoufik07 4d0382d580 Lint 2019-02-19 13:33:17 +01:00
taoufik07 a0dd7481ec Add tests 2019-02-17 19:39:36 +01:00
taoufik07 1c91480b0c Multiple cookies and directives 2019-02-17 19:35:34 +01:00
Taoufik 85e5ec0a9a Merge pull request #288 from taoufik07/starlette>=0.10.2
Update Pipfile.lock and starlette==0.10.*
2019-02-14 13:49:57 +00:00
Taoufik 4ac04b0abc Merge pull request #290 from josegonzalez/patch-1 2019-02-14 13:49:07 +00:00
Taoufik d7e64a6e39 Merge pull request #289 from taoufik07/patch-20 2019-02-14 12:39:40 +00:00
Taoufik 17d526632e Merge pull request #285 from taoufik07/patch-19
Await for background task
2019-02-14 12:34:21 +00:00
Jose Diaz-Gonzalez 43da481df7 fix: always respect the configured session_cookie
The `session_cookie` was refactored to be set via a hardcoded `DEFAULT_SESSION_COOKIE` static variable, and this change will allow future cookie changes to trickle to the Request object.
2019-02-13 10:54:07 -05:00
Taoufik 5f5402833b Typos 2019-02-13 15:09:48 +00:00
taoufik07 d59c4333f2 starlette 0.10.* 2019-02-13 16:03:20 +01:00
taoufik07 49114f36ce Update starlette>=0.10.2 2019-02-13 12:14:13 +01:00
taoufik07 b2039d99f3 Update Pipfile.lock 2019-02-13 12:13:16 +01:00
Taoufik 94fd86fee0 Await for background task 2019-02-11 08:45:48 +00:00
kennethreitz d70fdd3301 todo 2019-02-09 06:45:25 -06:00
kennethreitz 05b75efb43 version 2019-01-12 07:07:50 -05:00
kennethreitz be56e92d65 Merge pull request #277 from amikrop/master
Make `_route_for` shorter
2019-01-12 07:06:31 -05:00
kennethreitz 69eb843604 fixes 2019-01-12 06:57:28 -05:00
Aristotelis Mikropoulos 84a7f0e90b Make _route_for shorter 2019-01-11 03:46:35 +02:00
kennethreitz d1e105a29a Merge pull request #276 from tomchristie/resolve-startup-and-shutdown-events
Resolve startup/shutdown events
2019-01-09 17:49:49 -05:00
Tom Christie 9f0a568fa3 Resolve startup/shutdwown events 2019-01-09 12:42:15 +00:00
kennethreitz 05b46cbe34 Merge pull request #269 from erm/lifespan-handler
Return lifespan handler in dispatch method
2018-12-29 07:05:41 -05:00
kennethreitz c045586997 Merge pull request #267 from valtyr/graphql-context
Add Request and Response to GraphQL context
2018-12-29 07:05:26 -05:00
kennethreitz 8f0707f697 Merge pull request #262 from carlodri/carlodri-patch-1
include LICENSE il sdist
2018-12-29 07:05:12 -05:00
kennethreitz 36929b265c Merge pull request #263 from tkamenoko/patch-3
fix docstring of `API.url_for`
2018-12-29 07:05:05 -05:00
kennethreitz 734ba64965 Merge pull request #264 from sangheestyle/master
fix typo
2018-12-29 07:04:53 -05:00
kennethreitz 148e6742df Merge pull request #268 from taoufik07/patch-18
Websocket docs
2018-12-29 07:04:45 -05:00
Jordan bcb7e8f4f3 Update lock file 2018-12-28 18:10:27 +11:00
Jordan f678112099 Return lifespan handler in dispatch method 2018-12-28 17:46:27 +11:00
Taoufik 60b0c5f256 Update 2018-12-25 01:32:38 +00:00
Taoufik c8627939de Update 2018-12-25 01:18:44 +00:00
Taoufik 9144f0158a Websocket docs 2018-12-25 01:15:54 +00:00
Valtýr Örn Kjartansson d541aca80f Mention the GraphQL context object in docs 2018-12-23 13:28:50 +00:00
Valtýr Örn Kjartansson c73b2b8d34 Add request and response to GraphQL context
This allows the GraphQL resolvers access to things like session, headers etc.
Also enables creation of powerful GraphQL middleware.
2018-12-23 13:14:15 +00:00
Kim, Sanghee e2493b489d fix typo 2018-12-20 17:54:15 +02:00
T.Kameyama 8dee28ac7c fix docstring of API.url_for 2018-12-17 19:23:33 +09:00
Carlo cdd3885a0c include LICENSE il sdist 2018-12-12 22:35:41 +01:00
kennethreitz 1a28d528d0 Merge pull request #233 from taoufik07/websocket-x.x
WebSocket returns
2018-12-12 03:59:24 -05:00
kennethreitz 3ba12b8cee Merge pull request #230 from tkamenoko/file-form
formats: format_file returns filename, data, and content-type [Breaking]
2018-12-12 03:59:10 -05:00
kennethreitz 5a29ab6917 Merge pull request #257 from ibnesayeed/lifespan
Import lifespan as a middleware as per the change in starlette package
2018-12-12 03:58:53 -05:00
kennethreitz 694144a0c8 Merge pull request #244 from barrust/route-isclass-fix
fix for route.is_class_based
2018-12-12 03:58:41 -05:00
kennethreitz 8bed8e8741 Merge pull request #249 from abstiles/fix-broken-test-500
Fix broken exception handling in test_500
2018-12-12 03:58:06 -05:00
kennethreitz a81a348bce Merge pull request #245 from cdfuller/add-monospace-default
Add monospace fallback for preformatted text in docs
2018-12-12 03:57:55 -05:00
kennethreitz fd9e8c5cbc Merge pull request #240 from barrust/persist-debug
persist debug from API to run
2018-12-12 03:57:19 -05:00
kennethreitz 8030b1919d Merge pull request #253 from TomFaulkner/patch-1
Grammatical fix
2018-12-12 03:56:52 -05:00
Sawood Alam 72c789fdd7 Update Pipfile.lock to reflect version changes 2018-11-30 12:30:28 -05:00
Sawood Alam 1113a9aa0d Import lifespan as a middleware as per the change in starlette package 2018-11-30 11:52:43 -05:00
Tom Faulkner a5532614a2 Grammatical fix
Grammar: `into to` to `into`
2018-11-26 19:30:20 -06:00
Andrew Stiles 122023fb70 Restore the removal of ExceptionMiddleware
Back out the changes made to API and fix the test with a non-raising
instance of TestClient.
2018-11-21 13:05:51 -06:00
Andrew Stiles b8fa923ec9 Fix broken exception handling in test_500 2018-11-21 11:30:31 -06:00
Tyler Barrus ae06b3e01a default cont to False 2018-11-20 10:38:15 -05:00
Cody Fuller 5599ec2809 Add monospace fallback for preformatted text 2018-11-20 09:37:30 -06:00
Tyler Barrus e795cbddb6 fix for route.is_class_based 2018-11-20 10:26:29 -05:00
Tyler Barrus 0cb087c37b persist debug from API to run 2018-11-19 23:37:04 -05:00
taoufik07 983cbcc711 cleanup 2018-11-17 21:40:17 +01:00
taoufik07 6d154b0c78 WIP websocket 2018-11-17 20:49:26 +01:00
Tessei Kameyama f3f36e28c4 [formats] format-files detects file or simple-data
if `part` has `content-type`, `format_files` returns
`{"name" : filedata}` . `filedata` is `dict`, the keys are
`filename, content, content-type` .

Otherwise, `format_files` just returns `{"name" : b"data"}` .
2018-11-16 23:50:00 +09:00
Tessei Kameyama fdf4797726 [test] update file-upload [wip]
if form-data don't have content-type,
`req.media("files")` returns `{"name" : b"data"}` , else
`{"name" :
{"filename" : "foo.txt",
"content" : b"data",
"content-type" : "some/type"} }` .

not implemented yet.
2018-11-16 22:13:05 +09:00
Tessei Kameyama 67d8a3be98 [fix] typo 2018-11-16 20:23:09 +09:00
kennethreitz 4001a60f6c Merge pull request #229 from tomchristie/run-in-threadpool
Use Starlette's run_in_threadpool
2018-11-15 06:24:34 -05:00
kennethreitz d94db41271 Merge branch 'master' of https://github.com/kennethreitz/responder 2018-11-15 06:25:42 -05:00
kennethreitz 8abb78bb58 fix for #215 2018-11-15 06:22:04 -05:00
kennethreitz a80db99aa3 Merge pull request #228 from mmanhertz/patch-2
Fix issue in OpenAPI Schema Support example
2018-11-15 05:49:07 -05:00
Tom Christie 69a300f21a Use Starlette's run_in_threadpool 2018-11-15 10:38:05 +00:00
Matthias Manhertz 1b024b8092 Fix issue in OpenAPI Schema Support example
`PetSchema().dump()` returns a dict. Calling `.data` on it causes an AttributeError.
2018-11-15 11:19:11 +01:00
kennethreitz a622689597 Merge pull request #225 from peerster/patch-1
Update README
2018-11-14 06:18:13 -05:00
Pär 9943e66c49 Updates all of the python-responder.org links 2018-11-14 11:10:29 +01:00
Pär 7233c08281 Update README
Update link to python-responder.org to use https
2018-11-14 11:07:18 +01:00
kennethreitz 0845d92fda Merge pull request #223 from jkermes/master
Fix static response
2018-11-13 09:33:34 -05:00
Julien Kermes 1cc02e5a83 Fix static response 2018-11-13 15:18:30 +01:00
kennethreitz aa4cd7a144 Merge pull request #222 from MRSharff/debian-manifest-typofix
Fixed a typo in setup.py: mainfest -> manifest
2018-11-13 07:10:34 -05:00
Mathew Sharff b42ae0dfd7 Fixed a typo in setup.py: mainfest -> manifest 2018-11-12 21:33:12 -08:00
kennethreitz a6bd179726 version bump 2018-11-11 16:58:07 -05:00
kennethreitz aac7b5117b Merge pull request #218 from tomchristie/patch-1
Bring testominials in line with README
2018-11-10 17:48:29 -05:00
Tom Christie 8e8e99ed2e Bring testominials in line with README 2018-11-09 18:25:53 +00:00
kennethreitz 078ac23b20 Merge pull request #213 from mmanhertz/fix-212
fix for #212
2018-11-07 17:25:14 -05:00
kennethreitz 8e61df6b6a Merge pull request #207 from taoufik07/await_backgroundtask_raise
Await for background task and raise
2018-11-07 17:25:02 -05:00
mmanhertz cf7fb56653 fix for #212 2018-11-07 17:05:18 +01:00
taoufik07 da20d13c49 Await for background task and raise 2018-11-06 17:27:04 +01:00
kennethreitz 7a250aa8fc Merge branch 'master' of github.com:kennethreitz/responder 2018-11-06 05:46:25 -05:00
kennethreitz af28ecb82d fix for #200 2018-11-06 05:46:17 -05:00
kennethreitz 39a0e52a2a Merge pull request #201 from mmanhertz/patch-1
Fix OpenAPI version in examples
2018-11-06 05:12:51 -05:00
Matthias Manhertz a4f5a111c7 Fix OpenAPI version in examples
The example for "OpenAPI Schema Support" and "Interactive Documentation had `openapi="3.0"` which would cause the following error when swagger UI tried to render the schema:
```
Unable to render this definition
The provided definition does not specify a valid version field.

Please indicate a valid Swagger or OpenAPI version field. Supported version fields are swagger: "2.0" and those that match  openapi: 3.0.n (for example, openapi: 3.0.0).
```
2018-11-06 10:46:30 +01:00
kennethreitz c65e585493 Merge pull request #192 from vlcinsky/fix_doc_typo
Fixed missing `.data` on Marshmallow dump
2018-11-05 17:40:06 -05:00
kennethreitz c9e94561aa Merge pull request #196 from xyb/patch-1
fix Dockerfile
2018-11-05 14:09:13 -05:00
Xie Yanbo 1daa4c202b fix Dockerfile 2018-11-06 00:47:09 +08:00
kennethreitz 83ff361672 Merge pull request #195 from taoufik07/fix/routes_conflict
Fix/routes conflict
2018-11-05 09:01:15 -05:00
taoufik07 2c686f107d Tests 2018-11-05 14:52:08 +01:00
taoufik07 ac8ec3d5ef cleanup 2018-11-05 14:51:56 +01:00
taoufik07 21e70ef913 Fix tests 2018-11-05 14:48:34 +01:00
taoufik07 c4f5b0e7c2 Fix routes conflict 2018-11-05 14:47:46 +01:00
Jan Vlcinsky 45d6c1389d Fixed missing .data on Marshmallow dump 2018-11-04 22:06:54 +01:00
kennethreitz 0c9bc5a3af fix 2018-11-04 07:22:03 -05:00
kennethreitz 5b67d5a04a Merge branch 'master' of github.com:kennethreitz/responder 2018-11-04 07:20:41 -05:00
kennethreitz f3396a5573 fix for #188 2018-11-04 07:20:30 -05:00
kennethreitz e9f48788a3 Merge pull request #184 from rparrapy/change-route-cache
Replace memoize decorator by functools.lru_cache
2018-11-02 07:09:08 -04:00
Rodrigo Parra 6993a1ea46 Replace memoize decorator by functools.lru_cache 2018-11-02 06:32:01 -03:00
kennethreitz 8bcfb4585b cleanup testimonials 2018-11-01 05:38:22 -04:00
kennethreitz db45251f7f Merge pull request #182 from rparrapy/patch-1
Fix typo
2018-10-31 17:39:02 -04:00
kennethreitz 5582667b4f deployment 2018-10-31 17:37:13 -04:00
Rodrigo Parra 2c898aaf23 Fix typo 2018-10-31 14:36:40 -03:00
kennethreitz e0999ffcdd Merge branch 'master' of github.com:kennethreitz/responder 2018-10-31 06:10:49 -04:00
kennethreitz 03811768bb depoloyment 2018-10-31 06:10:40 -04:00
kennethreitz fbef577c9f Merge pull request #179 from taoufik07/patch-16
Typo :3
2018-10-30 21:56:18 -04:00
Taoufik 9434510ce9 Typo :3 2018-10-31 01:04:25 +01:00
kennethreitz 354130c151 .serve 2018-10-30 19:08:57 -04:00
kennethreitz 3e3cba016a Merge pull request #177 from tomchristie/patch-1
Update api.py
2018-10-30 10:37:38 -04:00
Tom Christie f75e120bef Update api.py
Refs https://github.com/kennethreitz/responder/issues/174

You don't need both to use both DebugMiddleware and ExceptionMiddleware, just one or the other.

Currently errors will cause a traceback 500 page, then re-raise, then attempt to generate another traceback 500 page, resulting in the behavior commented on in #174
2018-10-30 14:34:10 +00:00
kennethreitz 1d0294e430 merge 2018-10-30 05:01:25 -04:00
kennethreitz f786dd8254 apispec 2018-10-30 04:59:24 -04:00
kennethreitz cd9d09fd53 Merge pull request #175 from taoufik07/patch-15
Set debug to False in tests
2018-10-29 19:04:32 -04:00
kennethreitz 7471bbcd4e Merge pull request #173 from taoufik07/patch-14
Trusted hosts docs
2018-10-29 19:04:19 -04:00
Taoufik 43b04eccbd Set debug to False in tests 2018-10-29 22:53:56 +01:00
Taoufik 6a5d0b5e9f Trusted hosts docs 2018-10-29 21:56:54 +01:00
kennethreitz 359d366de4 Merge pull request #172 from taoufik07/trustedhost_starlette
starlette's TrustedhostMiddleware
2018-10-29 16:33:35 -04:00
taoufik07 556d9f3a7b Update starlette 2018-10-29 21:08:38 +01:00
taoufik07 2cab2dcec0 Tests 2018-10-29 20:54:38 +01:00
taoufik07 99d4e78dc9 Use starlette TrustedHostMiddleware 2018-10-29 20:48:51 +01:00
kennethreitz 9aa99869ae next version 2018-10-29 08:00:44 -04:00
kennethreitz 08e0d87347 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-29 07:57:19 -04:00
kennethreitz 3f9e4057d3 run sync tasks in threadpoolexecutor 2018-10-29 07:54:59 -04:00
kennethreitz a29e40353c Merge pull request #170 from taoufik07/trusted_hosts
Trusted hosts support
2018-10-28 14:47:28 -04:00
taoufik07 778cb2dd0f Add Tests 2018-10-28 18:26:42 +00:00
taoufik07 f7d5514b94 Fix test base_url 2018-10-28 18:12:21 +00:00
taoufik07 954637f7b3 Pass base_url to the TestClient 2018-10-28 18:09:52 +00:00
taoufik07 1ab46104c8 Allow all hosts by default 2018-10-28 14:51:24 +00:00
kennethreitz 815776d473 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-28 05:25:56 -04:00
kennethreitz 8db1a7be90 Merge pull request #169 from taoufik07/patch-13
typo
2018-10-28 05:23:56 -04:00
taoufik07 7b11fa24dd Silence for now 2018-10-28 01:38:17 +01:00
taoufik07 1f0f2318d5 cleanup 2018-10-28 01:34:26 +01:00
taoufik07 029b3e2a52 Tests 2018-10-28 00:46:50 +01:00
taoufik07 4fff823def Trusted host 2018-10-28 00:46:39 +01:00
Taoufik cab78275f4 typo 2018-10-27 22:25:11 +01:00
kennethreitz 5f60e4fedb before_request 2018-10-27 09:22:17 -04:00
kennethreitz 96971a33a7 tour 2018-10-27 09:20:18 -04:00
kennethreitz 9a7409f521 test for before_request 2018-10-27 09:18:07 -04:00
kennethreitz 80aa7e305b before_request=True 2018-10-27 09:15:52 -04:00
kennethreitz 27d513cb01 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-27 09:04:29 -04:00
kennethreitz 9bf5cc8c03 before_request, v1 2018-10-27 09:04:19 -04:00
kennethreitz 7994b210cd Merge pull request #166 from repodevs/fix-deployment-example
DOC: change dockerfile instruction `from` to use UPPERCASE
2018-10-27 07:33:47 -04:00
Edi Santoso 46555bbe3f DOC: change dockerfile instruction from to use UPPERCASE 2018-10-27 18:29:43 +07:00
kennethreitz 4d15dbc465 fix for sessions 2018-10-27 07:10:05 -04:00
kennethreitz 855d3c4320 cookies 2018-10-27 06:21:19 -04:00
kennethreitz 4564862acc xfail 2018-10-27 06:11:47 -04:00
kennethreitz 176dd70073 fix 301 redirects 2018-10-27 06:07:50 -04:00
kennethreitz a5e6f0c196 version 2018-10-27 05:39:17 -04:00
kennethreitz 083bb5a96c Merge branch 'master' of github.com:kennethreitz/responder 2018-10-27 05:13:39 -04:00
kennethreitz 04522281be don't do whitenoise index file 2018-10-27 05:13:30 -04:00
kennethreitz 0e8bb49b59 Merge pull request #164 from taoufik07/patch-12
Fix typo
2018-10-27 04:51:34 -04:00
Taoufik 9abf6eea16 typo 2018-10-26 23:57:16 +01:00
Taoufik 1d7a04ce7b Fix typo 2018-10-26 23:15:13 +01:00
kennethreitz 49fb5792c3 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-26 17:04:22 -04:00
kennethreitz 5eebba09c5 test redirects 2018-10-26 17:04:14 -04:00
kennethreitz b86974688e Merge pull request #163 from Hwesta/patch-1
Fix typo in tour.rst
2018-10-26 15:24:06 -04:00
Holly Becker 74afe2ed13 Fix typo in tour.rst 2018-10-26 12:19:33 -07:00
kennethreitz ed53a0b624 Merge pull request #161 from repodevs/fix-doc-quickstart
DOC: Fix quickstart response headers
2018-10-26 14:17:31 -04:00
kennethreitz 23e15d6459 Merge pull request #162 from sheb/patch-3
fix CI build failing since secret key has changed
2018-10-26 14:17:20 -04:00
Sébastien Geffroy 71ea19d1c1 fix CI build failing since secret key has changed 2018-10-26 20:15:05 +02:00
Edi Santoso fa621d076d DOC: Fix quickstart response headers 2018-10-27 00:53:11 +07:00
kennethreitz 4902f1328a Merge pull request #160 from frostming/graphql-doc
Fix doc about graphql usage.
2018-10-26 11:57:15 -04:00
kennethreitz 2ee8ff484d better 2018-10-26 11:09:24 -04:00
kennethreitz c872fe3c78 image 2018-10-26 11:08:37 -04:00
kennethreitz a08b275463 fix 2018-10-26 10:51:48 -04:00
kennethreitz 9717208dd4 v1.0.1 2018-10-26 10:51:35 -04:00
kennethreitz c9a233f5e5 api cleanup 2018-10-26 10:50:58 -04:00
kennethreitz 7389350ff9 fail 2018-10-26 10:48:12 -04:00
Frost Ming f46ac08cff Fix doc about graphql usage. 2018-10-26 22:03:43 +08:00
kennethreitz 296d5e7974 pipfile.lock 2018-10-26 09:19:31 -04:00
kennethreitz fe0bea686c simplify 2018-10-26 08:22:04 -04:00
kennethreitz 838d172512 v1.0.0 2018-10-26 08:10:49 -04:00
kennethreitz 2c02c51c37 fix docs 2018-10-26 08:09:56 -04:00
kennethreitz 67a4cbca2c move graphql to extension 2018-10-26 08:07:24 -04:00
kennethreitz a2f97e727f powered by starlette 2018-10-26 07:39:02 -04:00
kennethreitz 462506113e cleanup 2018-10-26 07:36:41 -04:00
kennethreitz 5f2a72203f cleanup things 2018-10-26 07:29:46 -04:00
kennethreitz d6febe2d02 test for startup 2018-10-26 07:01:28 -04:00
kennethreitz c2bd1e989a add_event_handler update 2018-10-26 06:55:33 -04:00
kennethreitz f886c2c050 cleanup 2018-10-26 06:54:15 -04:00
kennethreitz ae770e603a Merge branch 'master' into master 2018-10-26 06:48:42 -04:00
kennethreitz 7b79472d65 v0.3.3 2018-10-25 18:15:22 -04:00
kennethreitz 090a3a571b Merge pull request #156 from taoufik07/patch-11
Fix link formatting
2018-10-25 18:12:36 -04:00
kennethreitz f9d55fc425 Merge pull request #157 from steinnes/re-raise-exception-after-response
Improve exception handling
2018-10-25 18:12:24 -04:00
Steinn Eldjárn Sigurðarson 4f57e8a5d1 Improve exception handling
Re-raise exceptions caught in _dispatch_request.  Added starlette
ExceptionMiddleware to be able to test this gracefully.
2018-10-25 22:08:10 +00:00
Taoufik 1e6c9d935a Fix link formatting 2018-10-25 22:45:29 +01:00
kennethreitz 00cfde169b remove future ideas 2018-10-25 17:41:58 -04:00
kennethreitz 02733ac718 Merge pull request #155 from taoufik07/features/CORS
Custom CORS params and docs
2018-10-25 17:36:40 -04:00
Taoufik 55b55e62da Improvements 2018-10-25 20:58:01 +01:00
Taoufik 5fccedd4c4 Improvement 2018-10-25 20:55:14 +01:00
kennethreitz b9ad78ec79 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-25 15:44:07 -04:00
taoufik07 64ac6bcd1f Add CORS docs 2018-10-25 20:44:04 +01:00
kennethreitz 45e4d80c4d --pre 2018-10-25 15:44:01 -04:00
taoufik07 a5b1652d15 Custom CORS params via cors_params 2018-10-25 20:43:50 +01:00
kennethreitz f954eb7d88 Merge pull request #154 from taoufik07/features/CORS
CORS
2018-10-25 15:30:19 -04:00
taoufik07 53216813e5 Add CORSMiddleware 2018-10-25 20:12:21 +01:00
kennethreitz 1618203930 Merge pull request #153 from metakermit/address_doc
document how to customise the address
2018-10-25 13:40:58 -04:00
Dražen Lučanin 237a2ed426 document how to customise the address 2018-10-25 16:43:19 +02:00
kennethreitz d33289503a remove is_routed 2018-10-25 08:24:45 -04:00
kennethreitz f5ff4c9725 clean up 2018-10-25 08:23:22 -04:00
kennethreitz 62f932dcfc default secret key 2018-10-25 08:22:14 -04:00
kennethreitz b66112d0ca improvements 2018-10-25 08:20:06 -04:00
kennethreitz b98354e63a simplify readme 2018-10-25 07:31:34 -04:00
kennethreitz 94b3625718 ideas 2018-10-25 07:29:05 -04:00
kennethreitz f7ee720281 subtle improvements 2018-10-25 07:26:05 -04:00
kennethreitz 4ab523bf01 fix static 2018-10-25 07:25:35 -04:00
kennethreitz 2d4f1bfd02 test static 2018-10-25 07:10:51 -04:00
kennethreitz 38426c9143 for tests 2018-10-25 07:10:42 -04:00
kennethreitz bdf151e0a7 test responder docs 2018-10-25 07:08:45 -04:00
kennethreitz 9768b7888d Merge branch 'master' of github.com:kennethreitz/responder 2018-10-25 06:59:05 -04:00
kennethreitz 740a48566f important stuff 2018-10-25 06:58:57 -04:00
kennethreitz 475cd1a106 removed 2018-10-25 06:47:58 -04:00
kennethreitz 38e7c39d69 Merge pull request #151 from tkamenoko/patch-2
fix: license mismatch in setup.py
2018-10-25 06:38:45 -04:00
T.Kameyama 774db6bead fix: license mismatch in setup.py 2018-10-25 18:52:00 +09:00
kennethreitz a1a3e0412a Merge pull request #148 from taoufik07/patch-10
Quick fix
2018-10-24 19:09:59 -04:00
Taoufik 0b39c89e60 Quick fix 2018-10-24 19:17:53 +01:00
kennethreitz 53be4d8954 index.rst 2018-10-24 10:32:28 -04:00
kennethreitz 03812cc7eb v0.3.1 2018-10-24 08:34:46 -04:00
kennethreitz aa12b24293 whitenoise 2018-10-24 08:34:06 -04:00
kennethreitz daf43009ba changelog 2018-10-24 08:21:08 -04:00
kennethreitz 955d777ca5 Merge pull request #146 from JayjeetAtGithub/master
Improve readability
2018-10-24 08:19:00 -04:00
kennethreitz cc9472aa2f Merge pull request #147 from taoufik07/fix/default_endpoint_and_route_found
Do not return default endpoint if the route is found
2018-10-24 08:18:38 -04:00
kennethreitz e527f3cb1f interactive documentation 2018-10-24 08:19:28 -04:00
kennethreitz 3a375a8975 documentation 2018-10-24 08:17:26 -04:00
kennethreitz 2698496592 documentation endpoint 2018-10-24 08:16:55 -04:00
kennethreitz 0155d854e3 v0.3.0 2018-10-24 08:16:22 -04:00
kennethreitz c74cc8586f improve imports 2018-10-24 08:15:21 -04:00
kennethreitz 8eb89da9a0 improve imports 2018-10-24 08:14:24 -04:00
kennethreitz dee6ee3cef whitenoise, apistar 2018-10-24 08:13:15 -04:00
kennethreitz beab89df09 apistar 2018-10-24 08:12:45 -04:00
kennethreitz 5164d4ec32 apistar 2018-10-24 08:12:39 -04:00
taoufik07 878db851af Return default endpoint if the route is not found 2018-10-24 12:47:47 +01:00
Jayjeet Chakraborty 686ff72ae0 Improve readability 2018-10-24 16:37:02 +05:30
kennethreitz 2710d7098f v0.2.3 2018-10-24 07:02:44 -04:00
kennethreitz 7f41ff4035 Merge pull request #138 from taoufik07/fix/cbv
Fix CBV
2018-10-24 06:59:51 -04:00
kennethreitz ed8d51014c Merge branch 'master' into fix/cbv 2018-10-24 06:57:28 -04:00
kennethreitz d09a51f47d Merge pull request #140 from taoufik07/patch-9
Typo
2018-10-24 06:56:53 -04:00
kennethreitz 59bae90454 Merge pull request #142 from taoufik07/fix/static_response
Fix static response
2018-10-24 06:56:42 -04:00
kennethreitz 13ee0ca94e Merge pull request #136 from taoufik07/fix/Route.is_function
Fix Route.is_function
2018-10-24 06:56:24 -04:00
kennethreitz 5abc095050 Merge pull request #139 from JayjeetAtGithub/master
Fix Typo in api.py
2018-10-24 06:56:02 -04:00
kennethreitz 7eb68c8388 Merge pull request #143 from frostming/patch-1
Typo in tour.rst
2018-10-24 06:55:50 -04:00
Frost Ming f69b644a77 Typo in tour.rst 2018-10-24 12:28:11 +08:00
Peder Bergebakken Sundt 6b93125ff2 Add support for "tick" in api.on_event 2018-10-24 06:26:58 +02:00
Peder Bergebakken Sundt 43faef4569 Add api.on_event decorator supporting startup and cleanup 2018-10-24 06:25:44 +02:00
taoufik07 fe41d4c863 Fix static response 2018-10-24 01:17:02 +01:00
Taoufik 29830455ed Typo 2018-10-24 00:11:27 +01:00
Jayjeet Chakraborty e50828093d Clean print statement 2018-10-24 02:56:43 +05:30
Jayjeet Chakraborty 880d29c5a9 Fix Typo in api.py 2018-10-24 02:46:33 +05:30
taoufik07 77b2e9ba7a tests 2018-10-23 21:20:09 +01:00
taoufik07 586fad7646 Fix CBV 2018-10-23 21:19:57 +01:00
kennethreitz fb636028fb improvements 2018-10-23 14:58:02 -04:00
taoufik07 a8c3f8fc46 Fix Route.is_function 2018-10-23 19:52:43 +01:00
kennethreitz 72f4227c5a remove redundancy in tour 2018-10-23 08:46:20 -04:00
kennethreitz 8ccace8ef9 testing 2018-10-23 08:36:20 -04:00
kennethreitz 6d40c6dfe5 assert 2018-10-23 08:34:32 -04:00
kennethreitz 0b5562cdec fix 2018-10-23 08:33:51 -04:00
kennethreitz eeff0816f3 doc updates 2018-10-23 08:29:02 -04:00
kennethreitz f1f16dea3f best practices for secret key 2018-10-23 08:14:00 -04:00
kennethreitz bfc6ef2049 test client docs 2018-10-23 08:12:40 -04:00
kennethreitz 5212de79d3 v0.2.2 2018-10-23 08:05:07 -04:00
kennethreitz b61c02e5df Merge pull request #132 from vuonghv/show-exception-background-task
Show traceback info when background tasks raise exceptions
2018-10-23 08:02:59 -04:00
kennethreitz f982954e8f versions 2018-10-23 08:03:28 -04:00
kennethreitz 3ba20e69ba requests session 2018-10-23 08:02:30 -04:00
kennethreitz aea01fd893 Revert "idk what's happening"
This reverts commit e34cb539d2.
2018-10-23 08:00:56 -04:00
kennethreitz 950be14eca Revert "Merge branch 'master' of github.com:kennethreitz/responder"
This reverts commit 446deffc17, reversing
changes made to e0863115ee.
2018-10-23 08:00:30 -04:00
kennethreitz 446deffc17 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-23 07:59:07 -04:00
kennethreitz e0863115ee use api.requests 2018-10-23 07:59:00 -04:00
kennethreitz e34cb539d2 idk what's happening 2018-10-23 07:58:48 -04:00
kennethreitz d8ade8638a merge 2018-10-23 07:57:23 -04:00
Vuong Hoang 3067080474 Show traceback when background tasks raise exceptions 2018-10-23 18:23:22 +07:00
kennethreitz 886cc0f214 Merge pull request #131 from daikeren/master
Fix Route.is_function
2018-10-23 07:01:40 -04:00
Andy Dai 071d34b016 Fix Route.is_function 2018-10-23 17:07:42 +08:00
kennethreitz a1564ca003 Merge pull request #123 from taoufik07/patch-7
Quick fix
2018-10-22 17:25:15 -04:00
Taoufik 60f0e765c2 Quick fix 2018-10-22 22:14:18 +01:00
kennethreitz 3f0ecea4bf Merge pull request #120 from Pentusha/master
Depend on marshmallow>=3.0.0b7
2018-10-22 17:06:00 -04:00
kennethreitz 2c9e6572c5 Update tour.rst 2018-10-22 17:05:50 -04:00
Ivan Larin 371a83f20f Depend on marshmallow>=3.0.0b7 kennethreitz/responder#119 2018-10-22 19:46:55 +03:00
kennethreitz b8cff1655a websockets 2018-10-22 10:18:37 -04:00
kennethreitz 232856ca3a Merge branch 'master' of github.com:kennethreitz/responder 2018-10-22 10:07:38 -04:00
kennethreitz 3f168ac6fd slots 2018-10-22 10:06:55 -04:00
kennethreitz c59cb1d0d3 websocket 2018-10-22 10:06:03 -04:00
kennethreitz ec13df75d0 kinda test websocket support 2018-10-22 10:05:20 -04:00
kennethreitz 6fc02964ba cleanup 2018-10-22 09:59:38 -04:00
kennethreitz ed79e45680 Merge pull request #116 from tkamenoko/patch-1
doc: fix Class-based views
2018-10-22 09:30:14 -04:00
kennethreitz 1be983bf89 cleanup 2018-10-22 09:28:14 -04:00
T.Kameyama b09d6a9d04 doc: fix Class-based views
In Class-based views, each method needs `self` as 1st argument.
2018-10-22 14:37:55 +09:00
taoufik07 db143d845d cleanup 2018-10-21 18:17:56 +01:00
taoufik07 2e23501f9d Fix check_existing 2018-10-21 18:15:13 +01:00
taoufik07 bd6addcd3a Add websocket support 2018-10-21 18:00:25 +01:00
taoufik07 631e1fb604 Add WebSocket 2018-10-21 17:36:21 +01:00
kennethreitz 30ee6726a8 Merge pull request #113 from metakermit/extend-static-docs
Extend static=True docs
2018-10-21 06:25:56 -04:00
kennethreitz 1c397db9d8 cleanup 2018-10-21 06:23:02 -04:00
kennethreitz cc23ca80f4 Merge pull request #112 from Nitish18/feat/server_debug_mode
feat: added debug mode for uvicorn server
2018-10-21 06:20:16 -04:00
Dražen Lučanin 449379a0ed extend static=True docs 2018-10-21 11:58:54 +02:00
Nitish Chauhan b3208b1c5b feat: added debug mode for uvicorn server 2018-10-21 15:20:08 +05:30
kennethreitz 4df60b55a6 Merge pull request #110 from sheb/patch-2
fix an AttributeError when route does not exist
2018-10-20 13:54:58 -07:00
Sébastien Geffroy 379553a1a5 fix an AttributeError when route does not exist 2018-10-20 21:55:43 +02:00
kennethreitz a2eaa5c7b5 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-20 14:46:21 -04:00
kennethreitz 175c46e68c __version__ 2018-10-20 14:46:10 -04:00
kennethreitz a58cc11079 500 support 2018-10-20 14:45:52 -04:00
kennethreitz 218a375c27 test 500s 2018-10-20 14:45:33 -04:00
kennethreitz 567b1577c6 Merge pull request #108 from shyamjos/patch-1
Fixed minor spelling mistakes in changelog
2018-10-20 11:21:11 -07:00
kennethreitz 3c3687d11f Merge pull request #109 from taoufik07/patch-6
clean up
2018-10-20 11:21:00 -07:00
Taoufik 19dfac8340 clean up 2018-10-20 18:37:04 +01:00
kennethreitz b61feafe5a 500 on errrors 2018-10-20 13:36:06 -04:00
Shyam Jos 0c342c8b3e Corrected Spelling
Corrected Spelling
2018-10-20 23:01:57 +05:30
Shyam Jos dbcba8fad7 Fixed minor spelling mistakes in changelog
Fixed minor spelling mistakes in changelog
2018-10-20 22:24:44 +05:30
kennethreitz b8053e20f2 fix 2018-10-20 12:10:59 -04:00
kennethreitz 1896901aa8 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-20 12:10:19 -04:00
kennethreitz 383c9132ed improvement 2018-10-20 12:10:09 -04:00
kennethreitz 57b144c3e7 Merge pull request #107 from taoufik07/patch-5
Refactor Route._weight and f-strings everywhere
2018-10-20 07:08:51 -07:00
kennethreitz eed5365fe0 file upload support 2018-10-20 09:56:35 -04:00
kennethreitz f5905568c4 files support 2018-10-20 09:54:53 -04:00
kennethreitz 096099470e yay tests pass 2018-10-20 08:50:36 -04:00
kennethreitz e7ed7aca3c tests still pass 2018-10-20 08:23:10 -04:00
kennethreitz 6725b275b8 cleanup 2018-10-20 07:59:39 -04:00
kennethreitz 3447a7ef41 v0.1.5 2018-10-20 07:59:12 -04:00
kennethreitz 99f35fbea4 use querydict for form parsing 2018-10-20 07:57:27 -04:00
kennethreitz 5c9a3912a9 cached _content 2018-10-20 07:38:53 -04:00
Taoufik 5d43c0418c f-string 2018-10-19 23:13:21 +01:00
Taoufik 87c0076e12 use f-string
Every time I scroll through the README, it hurts me
2018-10-19 23:10:39 +01:00
Taoufik 95252ac697 Refactor 2018-10-19 23:06:55 +01:00
kennethreitz 5bb9f96701 cleanup 2018-10-19 05:11:07 -07:00
kennethreitz 750e9dfaa7 cleanup 2018-10-19 04:54:49 -07:00
kennethreitz f34f3c1661 v0.1.4 2018-10-19 04:17:06 -07:00
kennethreitz d4f83c978c improvements 2018-10-19 04:16:19 -07:00
kennethreitz 212f280c19 models 2018-10-19 03:10:39 -07:00
kennethreitz f3e2450636 models 2018-10-19 03:09:53 -07:00
kennethreitz d6d496018d fix 2018-10-19 03:08:15 -07:00
kennethreitz 78be7fc772 api 2018-10-19 03:00:41 -07:00
kennethreitz 6ebadd8469 new files 2018-10-19 02:19:38 -07:00
kennethreitz 557750c8d4 customizable cookie 2018-10-18 17:02:10 -07:00
kennethreitz e85ef27e6c Merge pull request #98 from pbsds/master
Store Jinja enviroment in between template render calls
2018-10-18 16:46:31 -07:00
kennethreitz 4ca961a1b4 Merge pull request #104 from metakermit/cli-build
CLI build command
2018-10-18 16:42:17 -07:00
kennethreitz 6a9110e9c1 Merge branch 'master' into cli-build 2018-10-18 16:40:20 -07:00
Dražen Lučanin 51ffce09ae fix cli setup 2018-10-18 23:48:09 +02:00
Peder Bergebakken Sundt 1c4e96b365 Add api.jinja_values_base:dict
This allows the user to add functions and values for use in all
templates, without needing to pass them on each render call.

As a side effect: The reference to `api` is still passed into the template view,
but this now yield to the values passed into api.template(), like one
would normally expect.
2018-10-18 20:47:59 +02:00
Peder Bergebakken Sundt 0db70e8edd Store jinja enviroment in between the template render calls
This allows the user to modify the jinja
enviroment, adding custom filters and such
2018-10-18 20:47:34 +02:00
Peder Bergebakken Sundt e46b3a5e19 Rename s to s_ in api.template_string()
Issue described in #76 applied here as well, however less propable.
Same fix as in a8fc78fcda
2018-10-18 20:47:14 +02:00
kennethreitz fdd3d4d85a sessions 2018-10-18 10:25:19 -07:00
kennethreitz 37c9cba42e version 2018-10-18 10:20:06 -07:00
kennethreitz c1544f66bb tour 2018-10-18 10:14:20 -07:00
kennethreitz d37f41f6a5 docstrings 2018-10-18 10:08:57 -07:00
kennethreitz b245dd2d51 Merge pull request #96 from kennethreitz/sessions
sessions
2018-10-18 10:05:16 -07:00
kennethreitz a1fcf11399 Merge pull request #95 from taoufik07/patch-4
Use HTTPSRedirectMiddleware
2018-10-18 10:04:41 -07:00
kennethreitz 8f876da245 sessions 2018-10-18 10:03:56 -07:00
Taoufik 23b8e5a2b3 Use HTTPSRedirectMiddleware 2018-10-18 18:01:41 +01:00
kennethreitz 3b7e7c7192 Merge pull request #94 from mathiasose/graphql-variables-and-operation-name
Find GraphQL variables, operation name from JSON
2018-10-18 07:49:34 -07:00
Mathias Ose b7ecf6e2e0 Find GraphQL variables, operation name from JSON
Make `_resolve_graphql_query` return *three* things from the JSON query: query (as before), variables and operation names. These values are all passed on to `schema.execute`.

TODO:
- Get variables and operation names from other requests types than JSON.
- Write tests.
- _Possibly_ refactor `_resolve_graphql_query` to return something a bit more structured than a 3-tuple.
2018-10-18 16:09:49 +02:00
kennethreitz 2ec6aaff03 docstrings 2018-10-18 06:42:26 -07:00
kennethreitz 19f8553f2d fix 2018-10-18 04:31:38 -07:00
kennethreitz 05a64ff095 cookies 2018-10-18 04:31:22 -07:00
kennethreitz a8fc78fcda fixes #76 2018-10-18 04:26:25 -07:00
kennethreitz e0e8b40fa2 Merge pull request #91 from kennethreitz/cookies
Cookies
2018-10-18 04:23:16 -07:00
kennethreitz 00165cd6ca tests for cookies 2018-10-18 04:16:44 -07:00
kennethreitz cd799ddfcd cookies 2018-10-18 04:07:13 -07:00
kennethreitz fffd6b7c86 Merge pull request #83 from kennethreitz/bnm_tests
added more tests to routes, and changed some bits in routes regarding regex
2018-10-18 02:51:55 -07:00
kennethreitz 439b008a34 Merge pull request #85 from condemil/patch-1
Add .python-version to .gitignore
2018-10-18 02:51:42 -07:00
kennethreitz f38e538892 Merge pull request #89 from pyasi/tests_for_status_codes
Add basic tests for the status codes file
2018-10-18 02:50:57 -07:00
Peter Yasi 6aa87a073f Add basic tests for the status codes file 2018-10-17 21:25:28 -04:00
Dmitry c38198ccba Add .python-version to .gitignore
.python-version allows to specify separate pyenv virtual environment
2018-10-17 22:58:16 +02:00
Luna 3be88c8cbf removed redundant import in routes.py 2018-10-17 21:17:14 +01:00
Luna 558ced1afb recommented pytest.ini addopts 2018-10-17 21:07:39 +01:00
Luna 0149e6935d added more tests to routes, and changed some bits in routes regarding regex 2018-10-17 21:05:38 +01:00
kennethreitz d97fdfd7c4 Merge pull request #75 from tomchristie/asgi-middleware
Support ASGI middleware
2018-10-17 12:03:15 -07:00
kennethreitz 8b85d8c6fb Merge pull request #80 from taoufik07/fix-CBV-missing-prams
Fix CBV missing params
2018-10-17 12:02:23 -07:00
kennethreitz 673779490c Merge pull request #82 from squiddy/patch-1
Fix docker image typo in deployment documentation
2018-10-17 12:01:43 -07:00
Reiner Gerecke 48154e7e2d Fix docker image typo in deployment documentation 2018-10-17 19:59:40 +02:00
taoufik07 20f72b3f63 Add tests 2018-10-17 18:43:24 +01:00
taoufik07 e82c958af2 Add missing params to on_method 2018-10-17 18:20:44 +01:00
taoufik07 60c311ab9f Add missing params to on_request 2018-10-17 18:20:16 +01:00
Tom Christie fbac81c245 Drop commented out gzip code 2018-10-17 15:13:09 +01:00
Tom Christie 9ca67d9228 Support ASGI middleware 2018-10-17 15:11:16 +01:00
kennethreitz 5ffa18221f an 2018-10-17 06:20:06 -07:00
kennethreitz aceb1f0f61 Must be awaited. 2018-10-17 06:17:21 -07:00
kennethreitz cee5ca8873 v0.1.1 2018-10-17 06:01:41 -07:00
kennethreitz d961d4ab43 default routes 2018-10-17 06:01:27 -07:00
kennethreitz 5205150a89 default route 2018-10-17 05:53:23 -07:00
kennethreitz 48e58cde5d docker 2018-10-17 05:19:22 -07:00
kennethreitz 033e91f8df name 2018-10-17 05:15:25 -07:00
kennethreitz aab3705897 myapi 2018-10-17 05:14:19 -07:00
kennethreitz d02efa81f2 deployment 2018-10-17 05:12:11 -07:00
kennethreitz 95a8240da7 single-page webapps 2018-10-17 04:58:11 -07:00
kennethreitz dd0ddab610 single page 2018-10-17 04:52:02 -07:00
kennethreitz d23ac10f90 version 2018-10-17 04:49:00 -07:00
kennethreitz ec18290b8a changelog 2018-10-17 04:48:38 -07:00
kennethreitz 2c4cd39dc9 static application support 2018-10-17 04:48:33 -07:00
kennethreitz 830bad0b85 docs for #53 2018-10-17 04:47:39 -07:00
kennethreitz f14ef6fa15 #53 2018-10-17 04:45:12 -07:00
kennethreitz 7400b1c83d static support #53 2018-10-17 04:38:51 -07:00
kennethreitz e7caf39fba static_route 2018-10-17 04:25:09 -07:00
kennethreitz 09fd0fb0ca version 2018-10-17 04:19:38 -07:00
kennethreitz 72adb13c0f version 2018-10-17 04:16:22 -07:00
kennethreitz ea0e382f82 test for #71 2018-10-17 04:15:36 -07:00
kennethreitz e70cba5143 Fix for #71 2018-10-17 04:12:13 -07:00
kennethreitz 8aec244c31 openapi 2018-10-17 04:12:03 -07:00
kennethreitz 60e163164f v0.0.8 2018-10-17 03:58:23 -07:00
kennethreitz 86b9b5f3fa graphiql 2018-10-17 03:57:39 -07:00
kennethreitz 401a208767 changelog 2018-10-17 03:27:26 -07:00
kennethreitz a1bfbda05b Merge branch 'master' into cli 2018-10-17 03:15:49 -07:00
kennethreitz 7d1f991ce4 changelog 2018-10-17 02:52:22 -07:00
kennethreitz 1b10378f58 merge 2018-10-17 02:33:49 -07:00
kennethreitz 2bbb379994 Merge pull request #70 from rpost/patch-1
Fix typo
2018-10-17 05:27:59 -04:00
kennethreitz a835f119e1 Merge pull request #67 from goodbadwolf/patch-1
Fix typo in quickstart example
2018-10-17 05:27:47 -04:00
kennethreitz 91d8bac680 Merge pull request #65 from taoufik07/routes_matching
Routes matching for humans
2018-10-17 05:27:32 -04:00
kennethreitz 3db10a4ce8 Merge pull request #63 from pesap/fix-typo
Fix typo in docs
2018-10-17 05:26:19 -04:00
kennethreitz 590640645b Merge pull request #62 from ybv/master
Add status code test for class based view
2018-10-17 05:26:03 -04:00
kennethreitz 7f02bfdf0c Merge pull request #61 from ewjoachim/patch-1
Fix typo
2018-10-17 05:25:49 -04:00
Radek Postołowicz e5cef0d9c0 Fix typo 2018-10-17 10:08:59 +02:00
ArtemGordinsky 85f9c33b2b Integrate GraphiQL 2018-10-17 08:00:03 +02:00
Manish P Mathai 148a430da4 Fix typo in quickstart example 2018-10-16 22:36:54 -07:00
Taoufik f7657679ac A verbose name 2018-10-17 05:07:29 +01:00
taoufik07 f0479019c3 Order the routes based on the weight 2018-10-17 04:38:08 +01:00
taoufik07 a9a4ceaa78 Add weight to Route 2018-10-17 04:37:31 +01:00
pesap c55c905621 Fix typo 2018-10-16 17:23:47 -07:00
ybv 4db2289b7e Add status code rest for class based view 2018-10-16 22:39:09 +05:30
Joachim Jablon 93172ea1d0 Fix typo 2018-10-16 14:41:30 +02:00
kennethreitz 2d935542e1 v0.0.7, immutable response object 2018-10-16 05:24:20 -07:00
kennethreitz f309ad7746 cli 2018-10-16 05:14:48 -07:00
kennethreitz a7ec1364f4 features 2018-10-16 04:42:44 -07:00
kennethreitz eb71ced092 graphql 2018-10-16 04:42:25 -07:00
kennethreitz 712ad0a73b mount a wsgi app 2018-10-16 04:35:30 -07:00
kennethreitz 48c0b137d5 version 2018-10-16 04:30:03 -07:00
kennethreitz dfccfcc3e5 wsgi apps 2018-10-16 04:29:44 -07:00
kennethreitz 6abe667efb Merge branch 'master' of github.com:kennethreitz/responder 2018-10-16 04:27:50 -07:00
kennethreitz c2472215ab features 2018-10-16 04:27:40 -07:00
kennethreitz ac3c1e149c Update README.md 2018-10-16 04:22:29 -07:00
kennethreitz cdf989427a mount flaks apps 2018-10-16 04:20:29 -07:00
kennethreitz ebf129edd3 /cc @tomchristie 2018-10-16 03:59:43 -07:00
kennethreitz 08c30f4baf Merge pull request #55 from sloria/upgrade-apispec
Depend on apispec>=1.0.0b1
2018-10-16 06:14:25 -04:00
kennethreitz cf6bdc20ef Merge pull request #57 from frostming/fix-docstring
Remove wsgi mentions in docstring
2018-10-16 06:14:15 -04:00
frostming 3ece644af8 Remove wsgi mentions in docstring 2018-10-16 12:54:28 +08:00
Steven Loria 3991c82c91 Depend on apispec>=1.0.0b1
The current code depends on the `apispec.yaml_utils`
module, which only exists in apispec 1.0.0b.

Close #52
2018-10-15 19:46:46 -04:00
kennethreitz 9b635253f0 Update index.rst 2018-10-15 12:29:50 -04:00
kennethreitz b62f41336e Merge pull request #54 from tomchristie/testimonial
Testimonial
2018-10-15 12:28:28 -04:00
Tom Christie f7b777c79e Testimonial 2018-10-15 16:47:12 +01:00
kennethreitz d18fa8e42a v0.0.6 2018-10-15 08:56:32 -04:00
kennethreitz 525c62ad26 content-type 2018-10-15 08:56:02 -04:00
kennethreitz 4000a6a48c fix 2018-10-15 08:45:22 -04:00
kennethreitz 5b173ed4c4 tour 2018-10-15 08:44:45 -04:00
kennethreitz f56ad73565 tour 2018-10-15 08:42:57 -04:00
kennethreitz 003991c8c6 fix 2018-10-15 08:41:39 -04:00
kennethreitz e2a32afb80 next version 2018-10-15 08:28:10 -04:00
kennethreitz f305a69bb3 tour 2018-10-15 08:26:13 -04:00
kennethreitz 84e8babd9e cleanups 2018-10-15 08:23:38 -04:00
kennethreitz aeb46d9b54 open_api spec 2018-10-15 08:21:40 -04:00
kennethreitz fafe0bd8e4 changelog 2018-10-15 07:19:01 -04:00
kennethreitz 9a2ab45957 safe dump 2018-10-15 07:18:19 -04:00
kennethreitz 66978a8cdc test yaml and form too 2018-10-15 07:14:14 -04:00
kennethreitz 1636012700 json uploads 2018-10-15 07:11:57 -04:00
kennethreitz 09206ae1e4 .pre 2018-10-15 07:05:59 -04:00
kennethreitz 9188475746 remove contributors file 2018-10-15 07:04:30 -04:00
kennethreitz 34d158a632 cleanup homepage 2018-10-15 07:02:53 -04:00
kennethreitz c06e6aa5ca changelog 2018-10-15 07:01:49 -04:00
kennethreitz f4f670f048 fix pipfile and pipfile.lock 2018-10-15 06:56:27 -04:00
kennethreitz 778d742b6e new lockfile 2018-10-15 06:55:31 -04:00
kennethreitz c8392b65b6 remove cli, next version 2018-10-15 06:53:41 -04:00
kennethreitz c0ace9c2e5 fix all the format things 2018-10-15 06:52:10 -04:00
kennethreitz dfcab7dcbf fix #47 2018-10-15 06:41:47 -04:00
kennethreitz eb0870deb1 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-14 20:40:45 -04:00
kennethreitz 5b7ef34523 fix #45 2018-10-14 20:40:31 -04:00
kennethreitz 6ec728e466 Merge pull request #43 from sheb/fix-typo-route
fix typo in doc string
2018-10-14 17:22:15 -04:00
kennethreitz f12a562a08 Merge pull request #44 from kennethreitz/bnm_tests
added tests for models.QueryDict
2018-10-14 17:21:55 -04:00
Luna 17c4c95593 added tests for models.QueryDict 2018-10-14 18:20:50 +01:00
kennethreitz 9b72c90944 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-14 11:57:22 -04:00
kennethreitz ec34da60a1 more tests 2018-10-14 11:57:15 -04:00
Sébastien Geffroy daa4b6368a fix typo in doc string 2018-10-14 17:51:21 +02:00
kennethreitz 931a7a1a6c Merge pull request #39 from rcatajar/patch-1
Fix typo in quickstart documentation
2018-10-14 11:47:25 -04:00
kennethreitz 69d5790078 Merge pull request #42 from kennethreitz/bnm_tests
Cleaned up redundant test function
2018-10-14 11:46:08 -04:00
kennethreitz 7571c18a55 Merge pull request #40 from tselepakis/fix-memoize
(fix) memoization functionality
2018-10-14 11:45:04 -04:00
Luna ff7ce9bdd0 linter error 2018-10-14 16:43:16 +01:00
Luna e5fc801899 added boilerplate cli 2018-10-14 16:40:57 +01:00
Konstantinos Tselepakis b362aa6813 (fix) memoization functionality
* Keep an object level cache
* Use func name as part of the dictionary key
2018-10-14 18:09:43 +03:00
Luna 652b961ac8 fixed conflict 2018-10-14 15:41:16 +01:00
Luna 652713aec4 clean up 2018-10-14 15:40:29 +01:00
kennethreitz 387b2f166b cleanup 2018-10-14 09:11:47 -04:00
kennethreitz 164b4a056a no memoize routes 2018-10-14 09:10:39 -04:00
Romain Catajar 29e514fea6 Fix typo in quickstart documentation 2018-10-14 15:10:29 +02:00
kennethreitz 310fff78c6 no benchmarks 2018-10-14 08:55:01 -04:00
kennethreitz f2efdc007c no benchmarks 2018-10-14 08:54:37 -04:00
kennethreitz b3be767923 upgrades 2018-10-14 08:46:33 -04:00
kennethreitz e86f2f3873 media() 2018-10-14 08:17:17 -04:00
kennethreitz 13d84f73d4 fix template rendering 2018-10-14 07:59:18 -04:00
kennethreitz e31342d3ba fix 2018-10-14 07:53:47 -04:00
kennethreitz daf0538bf3 fix 2018-10-14 07:52:52 -04:00
kennethreitz 451ce8b0c7 fix 2018-10-14 07:48:22 -04:00
kennethreitz b8cce14705 fix 2018-10-14 07:48:07 -04:00
kennethreitz bf1c9c650e fix 2018-10-14 07:47:45 -04:00
kennethreitz 8f6387536c fix 2018-10-14 07:46:15 -04:00
kennethreitz 56535ece11 fix 2018-10-14 07:44:13 -04:00
kennethreitz f1767719cb paragraphs 2018-10-14 07:43:43 -04:00
kennethreitz c925b06114 margin-top 2018-10-14 07:43:01 -04:00
kennethreitz 402426884d try this 2018-10-14 07:40:22 -04:00
kennethreitz df6c8a5a75 quotes 2018-10-14 07:39:51 -04:00
kennethreitz 99f5ae7125 another fix 2018-10-14 07:39:20 -04:00
kennethreitz d50a1b7d07 fix 2018-10-14 07:38:30 -04:00
kennethreitz fab3bb76f7 let's see if this works 2018-10-14 07:38:19 -04:00
kennethreitz 5025c66bb2 less testimonial 2018-10-14 07:37:22 -04:00
kennethreitz 800c153e96 Merge pull request #38 from kennethreitz/bnm_tests
Tests clean up
2018-10-14 07:29:07 -04:00
Luna 71bbda0fb7 clean up 2018-10-14 12:25:15 +01:00
kennethreitz 6e6bac429a order 2018-10-14 07:24:48 -04:00
kennethreitz 1ce091a4d9 feature tour 2018-10-14 07:24:14 -04:00
Luna a8f889be74 restructured tests 2018-10-14 12:23:33 +01:00
kennethreitz 5f33c6bfee rendering a template 2018-10-14 07:23:10 -04:00
Luna 6a290c49d8 Merge remote-tracking branch 'origin' into bnm_tests 2018-10-14 12:21:23 +01:00
kennethreitz b304d5d784 real fix 2018-10-14 07:12:45 -04:00
kennethreitz cfe83b97d9 fix 2018-10-14 07:12:27 -04:00
kennethreitz 2fec2bf560 response headers 2018-10-14 07:11:14 -04:00
kennethreitz 73dc1a7839 ! 2018-10-14 07:02:22 -04:00
kennethreitz 66fe951831 python 2018-10-14 07:01:13 -04:00
kennethreitz 7991bcbf1a note 2018-10-14 06:59:26 -04:00
kennethreitz de9516563a await 2018-10-14 06:58:56 -04:00
kennethreitz 27fefb821c quickstart 2018-10-14 06:58:14 -04:00
kennethreitz c195894db9 yaml 2018-10-14 06:57:39 -04:00
kennethreitz 6777b4d370 data 2018-10-14 06:55:54 -04:00
kennethreitz 09269c22a2 fix 2018-10-14 06:46:21 -04:00
kennethreitz 2e24a2f079 cleanup 2018-10-14 06:45:23 -04:00
kennethreitz 5d9932dd61 more quickstart 2018-10-14 06:44:18 -04:00
kennethreitz 062064213a improvements to docs 2018-10-14 06:34:04 -04:00
kennethreitz a2ae3ffb2b comments 2018-10-14 06:27:13 -04:00
kennethreitz 6cb4a0a3eb fix for encodings 2018-10-14 06:26:20 -04:00
kennethreitz f17c49091f cleanup 2018-10-14 06:23:23 -04:00
kennethreitz c16afc07df raises 2018-10-14 06:23:09 -04:00
kennethreitz 1616a96b2c fix 2018-10-14 06:21:59 -04:00
kennethreitz 261601230a better tests 2018-10-14 06:21:41 -04:00
kennethreitz 453a38df54 docstrings 2018-10-14 06:08:50 -04:00
kennethreitz 5b004a849f really fix encodings 2018-10-14 06:05:41 -04:00
kennethreitz 29d811d3fd conflict 2018-10-14 06:04:44 -04:00
kennethreitz 36c5739318 fix encoding 2018-10-14 06:03:57 -04:00
kennethreitz b3f9c67d34 cli 2018-10-14 05:47:04 -04:00
kennethreitz bc8eb802f7 remove heroku example 2018-10-14 05:46:36 -04:00
kennethreitz a138eead74 Merge pull request #35 from frostming/fix-req-content
Fix request.content
2018-10-14 05:00:13 -04:00
Frost Ming a700a0e1b1 Fix request.content 2018-10-14 10:31:03 +08:00
kennethreitz 205a33a241 Merge pull request #29 from ArtemGordinsky/ignore_missing_accept_header
Don't break when "Accept" header is missing
2018-10-13 22:08:10 -04:00
kennethreitz c88fd94c8b Merge pull request #33 from javad94/master
fixed typos
2018-10-13 22:07:40 -04:00
kennethreitz a2b4e2e87c Merge pull request #30 from gdamjan/master
fix docstring to remove mention of Waitress
2018-10-13 22:07:20 -04:00
Javad 4a8f1e95ba fixed typos 2018-10-14 01:18:14 +03:30
Javad 3a847d921e fixed typo 2018-10-14 01:15:11 +03:30
Javad 806fdb9541 fixed typos 2018-10-14 01:05:29 +03:30
Luna cf1adbdb01 Merge branch 'master' into bnm_tests 2018-10-13 21:57:29 +01:00
Luna 349d08e799 added conftest 2018-10-13 21:56:52 +01:00
Damjan Georgievski d680c7ed83 fix docstring to remove mention of Waitress 2018-10-13 19:45:31 +02:00
Artem Gordinsky d4cb7a711b Don't break when "Accept" header is missing 2018-10-13 17:30:52 +02:00
kennethreitz bb6e19e7cd requirements.txt 2018-10-13 09:54:53 -04:00
kennethreitz 1c3ea53e63 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-13 09:50:19 -04:00
kennethreitz 88e17029c5 fixes 2018-10-13 09:50:13 -04:00
kennethreitz 588e91b19f fix 2018-10-13 09:46:34 -04:00
kennethreitz 8cc2e7b6f1 fix 2018-10-13 09:46:29 -04:00
kennethreitz 222353b532 version 2018-10-13 09:46:14 -04:00
kennethreitz b88b266fd5 profile 2018-10-13 09:40:33 -04:00
kennethreitz 60e6fb99af fix 2018-10-13 09:27:11 -04:00
kennethreitz 65b60e57b2 Merge pull request #27 from kennethreitz/pytest
added addopts in pytest
2018-10-13 09:26:40 -04:00
kennethreitz 16a8402bf4 api.static_url 2018-10-13 09:23:05 -04:00
Luna 5896411136 added addopts in pytest 2018-10-13 14:14:59 +01:00
kennethreitz 0bb74a7885 pytest.ini 2018-10-13 09:13:33 -04:00
kennethreitz 86dfb9231f oops 2018-10-13 09:10:51 -04:00
kennethreitz 7198ce3eb0 fomatting 2018-10-13 09:09:08 -04:00
kennethreitz 08fecf1eb2 Merge branch 'bnm_tests' 2018-10-13 09:08:16 -04:00
kennethreitz 3eda26ca94 Merge branch 'master' into bnm_tests 2018-10-13 09:07:44 -04:00
kennethreitz d907914c7c Merge pull request #25 from 0xflotus/patch-1
fixed Characteristics
2018-10-13 09:02:54 -04:00
kennethreitz 266ab48fed better tests 2018-10-13 09:03:20 -04:00
Luna 3325cffa91 added entry to gitignore 2018-10-13 14:02:17 +01:00
Luna 43469ac62a Merge branch 'master' of https://github.com/kennethreitz/responder into bnm_tests 2018-10-13 14:00:59 +01:00
0xflotus a5c953fdb6 fixed Characteristics 2018-10-13 15:00:38 +02:00
Luna 627c46e458 added test cases for routes.py 2018-10-13 13:59:46 +01:00
kennethreitz 205eb34adc cleanup 2018-10-13 08:51:01 -04:00
kennethreitz 125e14d377 responder 2018-10-13 08:50:23 -04:00
kennethreitz a51c8a700b fix 2018-10-13 08:49:15 -04:00
kennethreitz 94e0400ea1 v0.0.2 2018-10-13 08:42:20 -04:00
kennethreitz 47c5b84093 form parsing working 2018-10-13 08:41:28 -04:00
kennethreitz 8b1fbfd16d better 2018-10-13 08:24:08 -04:00
kennethreitz cceb698899 installing 2018-10-13 08:23:28 -04:00
kennethreitz 01741df10d the cake is a lie 2018-10-13 08:20:38 -04:00
kennethreitz f91ebf8baa Merge branch 'master' of github.com:kennethreitz/responder 2018-10-13 08:19:21 -04:00
kennethreitz 4dde076030 move installation up 2018-10-13 08:19:12 -04:00
kennethreitz 3491001b7f Update README.md 2018-10-13 08:15:00 -04:00
kennethreitz 2acec68649 __slots__ 2018-10-13 08:13:32 -04:00
kennethreitz 51dab27374 index.rst 2018-10-13 08:08:00 -04:00
kennethreitz 145f5041bf options 2018-10-13 08:02:17 -04:00
kennethreitz 6034505380 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-13 08:01:18 -04:00
kennethreitz 8533d74906 5042 2018-10-13 08:01:10 -04:00
kennethreitz b2ae57b982 port 2018-10-13 07:59:16 -04:00
kennethreitz 49ffe9bec9 Merge pull request #20 from aitoehigie/master
Fleshed out the benchmarks section of the README.md file
2018-10-13 07:57:16 -04:00
kennethreitz fe5d92674e google analytics 2018-10-13 07:53:13 -04:00
kennethreitz 197d28f5c7 index 2018-10-13 07:50:29 -04:00
kennethreitz cd48bb0789 update readme 2018-10-13 07:50:09 -04:00
kennethreitz 90fc411e9a : 2018-10-13 07:46:36 -04:00
kennethreitz c22b6a84aa an 2018-10-13 07:45:42 -04:00
kennethreitz 9b65642f05 async 2018-10-13 07:43:57 -04:00
kennethreitz 83547dce9c continuation? 2018-10-13 07:43:09 -04:00
kennethreitz efeecceb54 fix? 2018-10-13 07:41:35 -04:00
kennethreitz ba9b5a40d2 simplify 2018-10-13 07:37:16 -04:00
kennethreitz 47b5bda277 fix 2018-10-13 07:36:43 -04:00
kennethreitz a343b6b1b6 fix 2018-10-13 07:36:30 -04:00
kennethreitz 0fe48d3003 index 2018-10-13 07:33:19 -04:00
kennethreitz 23e3760b08 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-13 07:31:50 -04:00
kennethreitz 3d31905562 improve formats for json 2018-10-13 07:31:45 -04:00
kennethreitz 9638c5266b index.rst 2018-10-13 07:30:56 -04:00
kennethreitz ad7ce9f55a Merge pull request #23 from aaqaishtyaq/docs-badge
add documentation badge in Readme
2018-10-13 07:26:55 -04:00
kennethreitz b0baf3b85a readme 2018-10-13 07:26:18 -04:00
kennethreitz d4d3687882 readme 2018-10-13 07:24:19 -04:00
kennethreitz faf55ca191 readme 2018-10-13 07:21:52 -04:00
kennethreitz d5096a23fb async def 2018-10-13 07:06:55 -04:00
kennethreitz ed5841d201 test async function 2018-10-13 07:06:14 -04:00
kennethreitz bbfc095a00 gitignore 2018-10-13 07:00:53 -04:00
kennethreitz 0fcb68a13d bunk 2018-10-13 07:00:42 -04:00
kennethreitz f97744c098 async defenitions work 2018-10-13 06:59:23 -04:00
kennethreitz d1cfa8d27a safe_load 2018-10-13 05:55:46 -04:00
Aaqa Ishtyaq 218dcf25c1 add documentation badge in Readme 2018-10-13 12:31:18 +05:30
aitoehigie 06e06973a4 format the readme file 2018-10-13 03:24:57 +01:00
aitoehigie 6f73cfc5f2 format the readme file 2018-10-13 03:23:47 +01:00
aitoehigie 6db5bbeaee format the readme file 2018-10-13 03:22:55 +01:00
aitoehigie 6ef5077164 format the readme file 2018-10-13 03:21:53 +01:00
aitoehigie 45e1ed7022 format the readme file 2018-10-13 03:20:36 +01:00
aitoehigie c14b4535a6 format the readme file 2018-10-13 03:19:21 +01:00
aitoehigie 411631d2f8 format the readme file 2018-10-13 03:17:42 +01:00
aitoehigie f4c3690bd8 format the readme file 2018-10-13 03:15:49 +01:00
aitoehigie 56fdea6b5d update the benchmarks section 2018-10-13 03:05:12 +01:00
aitoehigie 8a5c053d39 Add a concise benchmarks section to the README.md file 2018-10-13 02:34:58 +01:00
kennethreitz 42870cfa23 memoize template rendering 2018-10-12 16:56:21 -04:00
kennethreitz 6cf256cc05 memoize routes 2018-10-12 16:48:41 -04:00
kennethreitz 9fec915f62 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-12 16:42:21 -04:00
kennethreitz f1d5ab73cd readme 2018-10-12 16:42:13 -04:00
kennethreitz cd62972945 Merge pull request #14 from taoufik07/feature/query_dict
Add a QueryDict to manage query params
2018-10-12 15:26:05 -04:00
kennethreitz 998d09170c Update README.md 2018-10-12 14:36:03 -04:00
kennethreitz 4ba57181ec Update README.md 2018-10-12 14:31:27 -04:00
kennethreitz 8b9d8bdc62 Update README.md 2018-10-12 14:30:44 -04:00
taoufik07 4291d42dc0 Merge branch 'master' into feature/query_dict 2018-10-12 18:55:17 +01:00
kennethreitz 79fcc1ce40 uvloop 2018-10-12 13:28:07 -04:00
kennethreitz bfc6778dca readme 2018-10-12 13:27:02 -04:00
kennethreitz 701e57c264 fix all the things 2018-10-12 13:25:38 -04:00
kennethreitz 163d025c0d fix tests 2018-10-12 13:15:53 -04:00
kennethreitz d9befc6d8c test 2018-10-12 13:13:13 -04:00
kennethreitz 9e50a4c241 background 2018-10-12 12:28:14 -04:00
kennethreitz 9b0cae3794 background 2018-10-12 12:26:04 -04:00
taoufik07 6160dfb2f7 Add a simple test 2018-10-12 17:01:23 +01:00
taoufik07 cd013cdb06 Add QueryDict 2018-10-12 17:01:06 +01:00
kennethreitz 26cc7c90e9 Merge branch 'asgi' of https://github.com/tomchristie/responder into asgi 2018-10-12 10:29:13 -04:00
kennethreitz f28ac3cf22 Merge pull request #12 from taoufik07/patch-1
Typo : nicities -> niceties
2018-10-12 10:27:37 -04:00
kennethreitz 58fec4b082 Merge pull request #13 from taoufik07/patch-2
Fix resolve_hello indentation
2018-10-12 10:27:26 -04:00
Taoufik b91805a5df Fix graphql test 2018-10-12 15:27:04 +01:00
Tom Christie 0fa0df1bdf Add Pipfile 2018-10-12 15:18:13 +01:00
Tom Christie 3f7cacee3e Updated Pipfile.lock with latest pipenv 2018-10-12 15:12:44 +01:00
Taoufik 72637fd650 FIx resolve_hello indentation and use f-string 2018-10-12 15:12:17 +01:00
Taoufik aba1284f8e Typo : nicities -> niceties 2018-10-12 15:03:49 +01:00
Tom Christie 179e1dc9e5 Update Pipfile.lock 2018-10-12 15:02:08 +01:00
kennethreitz 75879a494e Merge pull request #7 from CianciuStyles/fix-familar-typo
Change "familar" into "familiar"
2018-10-12 09:54:46 -04:00
kennethreitz 73b1ea4713 Merge pull request #10 from OdinTech3/patch-1
Fixed a mispelt word
2018-10-12 09:54:30 -04:00
kennethreitz 55dc991c13 Merge pull request #11 from MichaelPereira/patch-1
Small typo : cooke -> cookie
2018-10-12 09:54:20 -04:00
Tom Christie c30316588a Form parsing requires Starlette 102 2018-10-12 14:53:55 +01:00
Tom Christie db5d6e7481 Lowercase method for on_{method} 2018-10-12 14:47:20 +01:00
Tom Christie f8d52f58d4 Drop form parsing support until Starlette PR #102 is in 2018-10-12 14:36:44 +01:00
Tom Christie 227ee499e4 request.params for API compatability with existing codebase/tests 2018-10-12 14:35:31 +01:00
Michael Pereira dcdaf6a674 Small typo : cooke -> cookie 2018-10-12 09:32:38 -04:00
Tom Christie d524ba3a37 Merge remote-tracking branch 'upstream/master' into asgi 2018-10-12 14:28:33 +01:00
Nick Spirit da5e288476 Fixed a mispelt word 2018-10-12 09:27:52 -04:00
kennethreitz baad7cd60d fix rst 2018-10-12 09:15:03 -04:00
kennethreitz e9d6fc33fd rst 2018-10-12 09:13:53 -04:00
kennethreitz c2fa0899e9 responder 2018-10-12 09:12:34 -04:00
kennethreitz 2dc09ec1f2 build status 2018-10-12 09:09:42 -04:00
kennethreitz fba640976f gzip 2018-10-12 09:04:33 -04:00
kennethreitz 8e7df61a73 fixes 2018-10-12 09:00:52 -04:00
Tom Christie 41776cf2df Initial pass at ASGI support 2018-10-12 13:57:22 +01:00
kennethreitz 23983f0b75 hacktoberfest 2018-10-12 08:55:27 -04:00
kennethreitz 84b457ede5 fix 2018-10-12 08:31:46 -04:00
kennethreitz a906e0bf0c Merge branch 'master' of github.com:kennethreitz/responder 2018-10-12 08:26:28 -04:00
kennethreitz 3db1aad96a hacks 2018-10-12 08:26:20 -04:00
kennethreitz 9c909e7a2c Update README.md 2018-10-12 08:19:38 -04:00
kennethreitz ad2ef7cb33 readme improvements 2018-10-12 08:17:56 -04:00
kennethreitz c851510ca9 fix travis 2018-10-12 08:13:35 -04:00
kennethreitz 71a21c2059 license 2018-10-12 08:12:54 -04:00
kennethreitz d90537eb8d README 2018-10-12 08:12:02 -04:00
kennethreitz 8266a15df5 travis 2018-10-12 08:08:05 -04:00
kennethreitz 9fce286f92 working again 2018-10-12 08:06:32 -04:00
kennethreitz fc244922cc endpoint 2018-10-12 08:03:36 -04:00
kennethreitz fce87fe20c better docs 2018-10-12 08:03:15 -04:00
kennethreitz d420358248 fix 2018-10-12 08:01:23 -04:00
kennethreitz be829ff0ae requirements 2018-10-12 07:59:31 -04:00
kennethreitz 0c9e224d45 fix 2018-10-12 07:58:14 -04:00
kennethreitz 58158c4d2b 3.6 2018-10-12 07:47:41 -04:00
kennethreitz b1e4222c93 requirements for builds 2018-10-12 07:46:12 -04:00
kennethreitz 70e5a016a6 v0.0.1 2018-10-12 07:41:56 -04:00
kennethreitz 6d7e7809a4 models 2018-10-12 07:39:30 -04:00
kennethreitz 4afc1ced93 python 3.7 2018-10-12 07:37:52 -04:00
kennethreitz 3c1807f04f docstrings 2018-10-12 07:35:36 -04:00
kennethreitz 6e9adac871 changes on the horizon 2018-10-12 07:19:29 -04:00
kennethreitz 2c3a3b2e17 docstrings 2018-10-12 07:02:35 -04:00
kennethreitz c070cc3f1a web service 2018-10-12 06:58:00 -04:00
kennethreitz d47caebc97 space for methods 2018-10-12 06:56:43 -04:00
kennethreitz 224c2bbb2b yellow 2018-10-12 06:55:35 -04:00
kennethreitz 48f1a0545e frontend 2018-10-12 06:53:38 -04:00
kennethreitz ac5146dbce fixes 2018-10-12 06:52:27 -04:00
kennethreitz 4fa82f4d7a index 2018-10-12 06:51:43 -04:00
kennethreitz 4bdd3f9138 lightgrey 2018-10-12 06:50:41 -04:00
kennethreitz 86bffbf62d italics 2018-10-12 06:49:53 -04:00
kennethreitz f7b0fb3f66 grey 2018-10-12 06:47:50 -04:00
kennethreitz 971be488d5 hacks 2018-10-12 06:47:16 -04:00
kennethreitz f415e9814c important 2018-10-12 06:44:47 -04:00
kennethreitz 01575a0b8d more colors 2018-10-12 06:43:31 -04:00
kennethreitz a77492dae1 changes 2018-10-12 06:42:39 -04:00
kennethreitz 080d6d30da color 2018-10-12 06:41:53 -04:00
kennethreitz ec9b20f87c class in yellow 2018-10-12 06:39:48 -04:00
kennethreitz 7aa405c87d lightgrey 2018-10-12 06:38:52 -04:00
kennethreitz af6257d364 cleanup 2018-10-12 06:37:06 -04:00
kennethreitz f930cbb2c9 grey 2018-10-12 06:35:52 -04:00
kennethreitz 6a5a22f035 hacks 2018-10-12 06:34:10 -04:00
kennethreitz 53f87e5def hacks 2018-10-12 06:33:28 -04:00
kennethreitz a00687cc0f hacks 2018-10-12 06:32:49 -04:00
kennethreitz d1d66c0e78 hacks 2018-10-12 06:31:53 -04:00
kennethreitz ee51f50809 fixes 2018-10-12 06:30:32 -04:00
kennethreitz b067da2a1c hacks 2018-10-12 06:27:43 -04:00
kennethreitz 3db2c00cd8 read the docs 2018-10-12 06:25:24 -04:00
kennethreitz 2f52ccbe4e responder 2018-10-12 06:22:33 -04:00
Stefano Cianciulli 25e9888438 Change "familar" into "familiar" 2018-10-12 11:00:59 +01:00
kennethreitz fabe7b9427 css 2018-10-12 05:32:22 -04:00
kennethreitz 9e464c394d Merge branch 'master' of github.com:kennethreitz/responder 2018-10-12 05:25:08 -04:00
kennethreitz dcaf9b61d4 cleanup 2018-10-12 05:24:44 -04:00
kennethreitz ecf0b2e57b Merge pull request #6 from 0x49D1/master
Update README.md
2018-10-12 05:22:20 -04:00
Dmitry Pursanov df32660754 Update README.md
Just a several small spelling fixes
2018-10-12 12:48:27 +04:00
kennethreitz 8bf795c8e4 typo 2018-10-11 22:45:10 -04:00
kennethreitz 0fc765a1fd .css 2018-10-11 22:22:41 -04:00
kennethreitz 6e7d97e5c0 hacks 2018-10-11 22:20:54 -04:00
kennethreitz a33ac8ed5f fonts 2018-10-11 22:15:08 -04:00
kennethreitz 7ca264fabd hr 2018-10-11 22:11:36 -04:00
kennethreitz e75f195f7c hacks 2018-10-11 22:10:06 -04:00
kennethreitz d3efa8b80d font haqcks 2018-10-11 22:08:56 -04:00
kennethreitz 82b78b6022 fonts 2018-10-11 22:00:12 -04:00
kennethreitz d8d1787e6f not all the time 2018-10-11 21:08:42 -04:00
kennethreitz d40cad2064 fix 2018-10-11 20:58:41 -04:00
kennethreitz cf323db503 i got it dude 2018-10-11 19:51:17 -04:00
kennethreitz 39d9b05455 cleaned up format 2018-10-11 19:43:50 -04:00
kennethreitz 475c7a9571 new media api 2018-10-11 19:40:21 -04:00
kennethreitz 7c4b6cf4f7 media 2018-10-11 19:22:54 -04:00
kennethreitz 697807c2d7 more tests 2018-10-11 19:11:54 -04:00
kennethreitz 85d900727b Merge branch 'master' of github.com:kennethreitz/responder 2018-10-11 18:54:07 -04:00
kennethreitz fc0d811740 fixes 2018-10-11 18:53:51 -04:00
kennethreitz 513867d242 Update README.md 2018-10-11 18:22:39 -04:00
kennethreitz 07ba75d9d5 changes 2018-10-11 16:44:56 -04:00
kennethreitz 6fa5f9af0c typography 2018-10-11 16:41:46 -04:00
kennethreitz 3c796b95fd models 2018-10-11 15:26:34 -04:00
kennethreitz 6de212d4bc fix sidebars 2018-10-11 14:08:47 -04:00
kennethreitz bb539c4d28 fix 2018-10-11 14:06:44 -04:00
kennethreitz f5d491667e index 2018-10-11 14:06:21 -04:00
kennethreitz 042d9ebc6c tagline 2018-10-11 14:05:46 -04:00
kennethreitz ac69ccae5e index 2018-10-11 14:04:28 -04:00
kennethreitz 940ab9d762 better 2018-10-11 13:57:55 -04:00
kennethreitz 00bfdf0e3e less is more 2018-10-11 13:23:17 -04:00
kennethreitz f3bc57a566 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-11 13:21:51 -04:00
kennethreitz 50c9bc60f9 fixes 2018-10-11 13:21:42 -04:00
kennethreitz ba384bb12a Update README.md 2018-10-11 13:18:58 -04:00
kennethreitz fe9184048c Merge branch 'master' of github.com:kennethreitz/responder 2018-10-11 13:18:48 -04:00
kennethreitz 90083f945e cleanup 2018-10-11 13:18:40 -04:00
kennethreitz eee17ca20b Update README.md 2018-10-11 13:14:28 -04:00
kennethreitz b2d47abacd Update README.md 2018-10-11 13:13:56 -04:00
kennethreitz 548fb229af Update README.md 2018-10-11 13:13:22 -04:00
kennethreitz c00b259c43 Update README.md 2018-10-11 13:11:50 -04:00
kennethreitz 2dbd6ac451 small 2018-10-11 13:11:34 -04:00
kennethreitz aab82baad0 Update README.md 2018-10-11 13:11:20 -04:00
kennethreitz ea2c5c3025 docs 2018-10-11 12:59:27 -04:00
kennethreitz b391b5622f basics 2018-10-11 12:59:20 -04:00
kennethreitz ea3cb8aa7b artboards 2018-10-11 12:51:58 -04:00
kennethreitz 6e125f5f47 logos 2018-10-11 12:48:59 -04:00
kennethreitz 554500a314 assets 2018-10-11 12:26:57 -04:00
kennethreitz f1c9de7ace readme 2018-10-11 12:24:01 -04:00
kennethreitz 3bcfe89f2a updates to redirects 2018-10-11 12:22:33 -04:00
kennethreitz 151c7bd342 fix 2018-10-11 12:14:20 -04:00
kennethreitz b8d569129e more complex example 2018-10-11 12:13:53 -04:00
kennethreitz b421e2e925 cleanup 2018-10-11 12:09:12 -04:00
kennethreitz 79b09a5ae5 oh yeah 2018-10-11 12:07:35 -04:00
kennethreitz b0d3c2de09 readme 2018-10-11 11:58:11 -04:00
kennethreitz 3201a46003 switch order 2018-10-11 11:56:00 -04:00
kennethreitz 538c72f5bd dict 2018-10-11 11:55:12 -04:00
kennethreitz 6efe28bd54 fix 2018-10-11 11:54:02 -04:00
kennethreitz 7eaa95b7ee cleanup 2018-10-11 11:52:32 -04:00
kennethreitz d2f8b41e25 fix 2018-10-11 11:51:29 -04:00
kennethreitz 034aa19564 contributors 2018-10-11 11:50:47 -04:00
kennethreitz bd049d6263 Merge pull request #2 from serhii73/patch-1
Update test_responder.py
2018-10-11 11:48:07 -04:00
kennethreitz ffb7468b44 Merge pull request #3 from nicoddemus/patch-1
Fix typo in README
2018-10-11 11:47:53 -04:00
kennethreitz 6dfbde27ca fix 2018-10-11 11:47:48 -04:00
kennethreitz c6c0197d86 Merge branch 'master' into patch-1 2018-10-11 11:47:47 -04:00
kennethreitz 678596ace4 readme 2018-10-11 11:41:22 -04:00
kennethreitz 9295525b92 practice what you preach 2018-10-11 11:39:25 -04:00
kennethreitz fde2d5415f more usage 2018-10-11 11:38:26 -04:00
kennethreitz fac80383d9 grip 2018-10-11 11:37:05 -04:00
kennethreitz f4cfe5639a blend 2018-10-11 11:36:07 -04:00
kennethreitz 87749e4288 sentence 2018-10-11 11:35:21 -04:00
kennethreitz 787e056b7f python highlighting 2018-10-11 11:33:40 -04:00
kennethreitz 62bd3d905b days ago 2018-10-11 11:31:57 -04:00
kennethreitz 2122f1ef1c code example 2018-10-11 11:31:17 -04:00
Bruno Oliveira 0bed48e756 Fix typo in README 2018-10-11 12:24:58 -03:00
kennethreitz f2b2128675 fix server 2018-10-11 11:12:22 -04:00
kennethreitz c0c5770a89 tests 2018-10-11 10:52:13 -04:00
kennethreitz e7dc55edf5 more tests 2018-10-11 10:30:46 -04:00
kennethreitz 83fa6d6897 parameterized routes working 2018-10-11 10:29:15 -04:00
serhii73 5d636ee9c4 Update test_responder.py
Fix order import
2018-10-11 17:01:35 +03:00
kennethreitz 1fdda366dd tests 2018-10-11 06:37:02 -04:00
kennethreitz 671499bb43 working order 2018-10-10 11:07:11 -04:00
kennethreitz 749a7efdef templates working 2018-10-10 07:48:00 -04:00
kennethreitz 90eecdaa84 mount other wsgi apps 2018-10-10 07:14:29 -04:00
kennethreitz 39f9cbfdbb note 2018-10-10 06:42:57 -04:00
kennethreitz 5794ba890c README 2018-10-09 08:06:42 -04:00
kennethreitz 4a3bf3a1aa fixes 2018-10-09 08:06:22 -04:00
kennethreitz 1427ca0a35 pipfile cleanup 2018-10-09 07:39:05 -04:00
kennethreitz 5b7b0fcb8e cleanups 2018-10-09 07:33:38 -04:00
kennethreitz 6ac48de44c I 2018-10-09 07:30:35 -04:00
kennethreitz 674efa3052 readme 2018-10-09 07:30:19 -04:00
kennethreitz 152a7153d0 cargo culting 2018-10-09 07:28:36 -04:00
92 changed files with 63442 additions and 637 deletions
+3
View File
@@ -0,0 +1,3 @@
github: kennethreitz
thanks_dev: kennethreitz
custom: https://cash.app/$KennethReitz
+16
View File
@@ -0,0 +1,16 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
+53
View File
@@ -0,0 +1,53 @@
name: "Documentation"
on:
push:
branches: [ main ]
pull_request: ~
workflow_dispatch:
# Cancel redundant in-progress jobs.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
documentation:
name: "Documentation"
runs-on: ubuntu-latest
env:
UV_SYSTEM_PYTHON: true
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Set up uv
uses: astral-sh/setup-uv@v5
with:
version: "latest"
enable-cache: true
cache-dependency-glob: |
pyproject.toml
- name: Install package and documentation dependencies
run: uv pip install '.[docs]'
- name: Build static HTML documentation
run: sphinx-build -W --keep-going docs/source docs/build
- name: Deploy to GitHub Pages
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs/build
cname: responder.kennethreitz.org
+58
View File
@@ -0,0 +1,58 @@
name: "Tests"
on:
push:
branches: [ main ]
pull_request: ~
workflow_dispatch:
# Cancel redundant in-progress jobs.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: "Python ${{ matrix.python-version }}"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: [
"3.10",
"3.11",
"3.12",
"3.13",
"3.14",
"3.14t",
"pypy3.11",
]
env:
UV_SYSTEM_PYTHON: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Set up uv
uses: astral-sh/setup-uv@v5
with:
version: "latest"
enable-cache: true
cache-suffix: ${{ matrix.python-version }}
cache-dependency-glob: |
pyproject.toml
- name: Install package
run: uv pip install '.[develop,test]'
- name: Run tests
run: pytest
+18
View File
@@ -1 +1,19 @@
.venv*
.vscode/
.cache
.idea
.python-version
.coverage
.pytest_cache
.DS_Store
coverage.xml
.coverage*
__pycache__
tests/__pycache__
build
responder.egg-info/
dist/
app.py
app2.py
+515
View File
@@ -0,0 +1,515 @@
# 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/), and
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [v3.5.0] - 2026-03-24
### Added
- CI validation for Python 3.14, 3.14 free-threaded, and PyPy 3.11
- Marimo notebook mounting docs and example
- Type annotations for `routes.py`
- `uv.lock` for reproducible installs
### Changed
- Replaced deprecated `asyncio.iscoroutinefunction` with `inspect.iscoroutinefunction` ahead of Python 3.16 removal
- Narrowed broad `except Exception` to specific exceptions in response model serialization and websocket chat example
- Improved GraphQL API interface with expanded test coverage
- Code formatting cleanup via pyproject-fmt and ruff
- Dropped Python 3.9 from CI
### Fixed
- WSGI mount returning 400 when requesting the exact mount root path
- Werkzeug 3.1.7 compatibility for trusted host validation in tests
- `future.result` bare property access in background task test (now properly calls `future.result()`)
- OpenAPI template packaging and static file serving
- RST title underline warning breaking docs CI
### Removed
- Read the Docs configuration (docs hosted on GitHub Pages)
## [v3.4.0] - 2026-03-22
### Changed
- Upgraded to Starlette 1.0
- Added comprehensive docstrings across the codebase
- Expanded API reference documentation
## [v3.3.0] - 2026-03-22
### Added
- Full documentation rewrite: tutorials for REST APIs, SQLAlchemy, Flask migration
- Auth, WebSocket, middleware, and configuration guides
- Testing docs with prose, examples, and tips
- GitHub Pages deployment for docs
### Changed
- Reworked homepage prose
- Rewrote CLI and API reference docs
## [v3.2.0] - 2026-03-22
### Added
- Pydantic auto-validation: `request_model` validates input, returns 422 on failure
- Pydantic auto-serialization: `response_model` strips extra fields from responses
- Server-Sent Events: `@resp.sse` for real-time streaming
- `resp.stream_file()` for streaming large files without loading into memory
- `@api.after_request()` hooks
- `api.group("/prefix")` for route groups and API versioning
- `api.graphql("/path", schema=schema)` one-liner GraphQL setup
- `api = responder.API(request_id=True)` for automatic request ID generation
- Built-in rate limiter: `RateLimiter(requests=100, period=60).install(api)`
- MessagePack format support: `await req.media("msgpack")`
- `req.is_json`, `req.path_params`, `req.client` properties
- `api.exception_handler()` decorator for custom error handling
- Lifespan context manager support
- `uuid` and `path` route convertors
- PEP 561 `py.typed` marker
- Pydantic support for OpenAPI schema generation
### Changed
- Dependencies flattened: `pip install responder` gets everything
- Core deps reduced to starlette + uvicorn
- TestClient lazy-loaded (no httpx import in production)
- Before-request hooks can short-circuit by setting status code
- Removed poethepoet task runner
### Fixed
- Multipart parser losing headers when parts have multiple headers
- `url_for()` with typed route params (`{id:int}`)
- `resp.body` encoding crash on bytes content
- GraphQL text query missing `await`
- Streaming responses not sending Content-Type headers
- Python 3.9 compatibility for union type syntax
## [v3.0.0] - 2026-03-22
### Added
- Platform: Added support for Python 3.10 - Python 3.13
- CLI: `responder run` now also accepts a filesystem path on its `<target>`
argument, enabling usage on single-file applications.
- CLI: `responder run` now also accepts URLs.
### Changed
- Platform: Minimum Python version is now 3.9 (dropped 3.6, 3.7, 3.8)
- Dependencies: Dramatically reduced core dependency count (10 → 5)
- Removed `requests`, `requests-toolbelt`, `rfc3986`, `whitenoise`
- Moved `apispec` and `marshmallow` to `openapi` optional extra
- Replaced `rfc3986` with stdlib `urllib.parse`
- Replaced `requests-toolbelt` multipart decoder with `python-multipart`
- Replaced deprecated `starlette.middleware.wsgi` with `a2wsgi`
- Switched from WhiteNoise to ServeStatic
- Dependencies: Pinned `starlette[full]>=0.40` (was unpinned)
- GraphQL: Upgraded to `graphene>=3` and `graphql-core>=3.1`
(from `graphene<3` and `graphql-server-core`, which is unmaintained)
- GraphQL: Updated GraphiQL UI from 0.12.0 (2018) to 3.0.6 with React 18
- Extensions: All of CLI-, GraphQL-, and OpenAPI-Support modules are
extensions now, found within the `responder.ext` module namespace.
- Packaging: Migrated from `setup.py` to declarative `pyproject.toml`
### Removed
- Platform: Removed support for EOL Python 3.6, 3.7, 3.8
- Status codes: Removed deprecated `resume_incomplete` and `resume`
aliases for HTTP 308 (marked for removal in 3.0)
- CLI: `responder run --build` ceased to exist
### Fixed
- Routing: Fixed dispatching `static_route=None` on Windows
- uvicorn: `--debug` now maps to uvicorn's `log_level = "debug"`
- Tests: Fixed deprecated httpx TestClient usage
## [v2.0.5] - 2019-12-15
### Added
- Update requirements to support python 3.8
## [v2.0.4] - 2019-11-19
### Fixed
- Fix static app resolving
## [v2.0.3] - 2019-09-20
### Fixed
- Fix template conflicts
## [v2.0.2] - 2019-09-20
### Fixed
- Fix template conflicts
## [v2.0.1] - 2019-09-20
### Fixed
- Fix template import
## [v2.0.0] - 2019-09-19
### Changed
- Refactor Router and Schema
## [v1.3.2] - 2019-08-15
### Added
- ASGI 3 support
- CI tests for python 3.8-dev
- Now requests have `state` a mapping object
### Deprecated
- ASGI 2
## [v1.3.1] - 2019-04-28
### Added
- Route params Converters
- Add search for documentation pages
### Changed
- Bump dependencies
## [v1.3.0] - 2019-02-22
### Fixed
- Versioning issue
- Multiple cookies.
- Whitenoise returns not found.
- Other bugfixes.
### Added
- Stream support via `resp.stream`.
- Cookie directives via `resp.set_cookie`.
- Add `resp.html` to send HTML.
- Other improvements.
## [v1.1.3] - 2019-01-12
### Changed
- Refactor `_route_for`
### Fixed
- Resolve startup/shutdwown events
## [v1.2.0] - 2018-12-29
### Added
- Documentations
### Changed
- Use Starlette's LifeSpan middleware
- Update denpendencies
### Fixed
- Fix route.is_class_based
- Fix test_500
- Typos
## [v1.1.2] - 2018-11-11
### Fixed
- Minor fixes for Open API
- Typos
## [v1.1.1] - 2018-10-29
### Changed
- Run sync views in a threadpoolexecutor.
## [v1.1.0] - 2018-10-27
### Added
- Support for `before_request`.
## [v1.0.5]- 2018-10-27
### Fixed
- Fix sessions.
## [v1.0.4] - 2018-10-27
### Fixed
- Potential bufix for cookies.
## [v1.0.3] - 2018-10-27
### Fixed
- Bugfix for redirects.
## [v1.0.2] - 2018-10-27
### Changed
- Improvement for static file hosting.
## [v1.0.1] - 2018-10-26
### Changed
- Improve cors configuration settings.
## [v1.0.0] - 2018-10-26
### Changed
- Move GraphQL support into a built-in plugin.
## [v0.3.3] - 2018-10-25
### Added
- CORS support
### Changed
- Improved exceptions.
## [v0.3.2] - 2018-10-25
### Changed
- Subtle improvements.
## [v0.3.1] - 2018-10-24
### Fixed
- Packaging fix.
## [v0.3.0] - 2018-10-24
### Changed
- Interactive Documentation endpoint.
- Minor improvements.
## [v0.2.3] - 2018-10-24
### Changed
- Overall improvements.
## [v0.2.2] - 2018-10-23
### Added
- Show traceback info when background tasks raise exceptions.
## [v0.2.1] - 2018-10-23
### Added
- api.requests.
## [v0.2.0] - 2018-10-22
### Added
- WebSocket support.
## [v0.1.6] - 2018-10-20
### Added
- 500 support.
## [v0.1.5] - 2018-10-20
### Added
- File upload support
### Changed
- Improvements to sequential media reading.
## [v0.1.4] - 2018-10-19
### Fixed
- Stability.
## [v0.1.3] - 2018-10-18
### Added
- Sessions support.
## [v0.1.2] - 2018-10-18
### Added
- Cookies support.
## [v0.1.1] - 2018-10-17
### Changed
- Default routes.
## [v0.1.0] - 2018-10-17
### Added
- Prototype of static application support.
## [v0.0.10] - 2018-10-17
### Fixed
- Bugfix for async class-based views.
## [v0.0.9] - 2018-10-17
### Fixed
- Bugfix for async class-based views.
## [v0.0.8] - 2018-10-17
### Added
- GraphiQL Support.
### Changed
- Improvement to route selection.
## [v0.0.7] - 2018-10-16
### Changed
- Immutable Request object.
## [v0.0.6] - 2018-10-16
### Added
- Ability to mount WSGI apps.
- Supply content-type when serving up the schema.
## [v0.0.5] - 2018-10-15
### Added
- OpenAPI Schema support.
- Safe load/dump yaml.
## [v0.0.4] - 2018-10-15
### Added
- Asynchronous support for data uploads.
### Fixed
- Bug fixes.
## [v0.0.3] - 2018-10-13
### Fixed
- Bug fixes.
## [v0.0.2] - 2018-10-13
### Changed
- Switch to ASGI/Starlette.
## [v0.0.1] - 2018-10-12
### Added
- Conception!
[unreleased]: https://github.com/kennethreitz/responder/compare/v3.5.0..HEAD
[v3.5.0]: https://github.com/kennethreitz/responder/compare/v3.4.0..v3.5.0
[v3.4.0]: https://github.com/kennethreitz/responder/compare/v3.3.0..v3.4.0
[v3.3.0]: https://github.com/kennethreitz/responder/compare/v3.2.0..v3.3.0
[v3.2.0]: https://github.com/kennethreitz/responder/compare/v3.0.0..v3.2.0
[v3.0.0]: https://github.com/kennethreitz/responder/compare/v2.0.5..v3.0.0
[v2.0.5]: https://github.com/kennethreitz/responder/compare/v2.0.4..v2.0.5
[v2.0.4]: https://github.com/kennethreitz/responder/compare/v2.0.3..v2.0.4
[v2.0.3]: https://github.com/kennethreitz/responder/compare/v2.0.2..v2.0.3
[v2.0.2]: https://github.com/kennethreitz/responder/compare/v2.0.1..v2.0.2
[v2.0.1]: https://github.com/kennethreitz/responder/compare/v2.0.0..v2.0.1
[v2.0.0]: https://github.com/kennethreitz/responder/compare/v1.3.2..v2.0.0
[v1.3.2]: https://github.com/kennethreitz/responder/compare/v1.3.1..v1.3.2
[v1.3.1]: https://github.com/kennethreitz/responder/compare/v1.3.0..v1.3.1
[v1.3.0]: https://github.com/kennethreitz/responder/compare/v1.2.0..v1.3.0
[v1.2.0]: https://github.com/kennethreitz/responder/compare/v1.1.3..v1.2.0
[v1.1.3]: https://github.com/kennethreitz/responder/compare/v1.1.2..v1.1.3
[v1.1.2]: https://github.com/kennethreitz/responder/compare/v1.1.1..v1.1.2
[v1.1.1]: https://github.com/kennethreitz/responder/compare/v1.1.0..v1.1.1
[v1.1.0]: https://github.com/kennethreitz/responder/compare/v1.0.5..v1.1.0
[v1.0.5]: https://github.com/kennethreitz/responder/compare/v1.0.4..v1.0.5
[v1.0.4]: https://github.com/kennethreitz/responder/compare/v1.0.3..v1.0.4
[v1.0.3]: https://github.com/kennethreitz/responder/compare/v1.0.2..v1.0.3
[v1.0.2]: https://github.com/kennethreitz/responder/compare/v1.0.1..v1.0.2
[v1.0.1]: https://github.com/kennethreitz/responder/compare/v1.0.0..v1.0.1
[v1.0.0]: https://github.com/kennethreitz/responder/compare/v0.3.3..v1.0.0
[v0.3.3]: https://github.com/kennethreitz/responder/compare/v0.3.2..v0.3.3
[v0.3.2]: https://github.com/kennethreitz/responder/compare/v0.3.1..v0.3.2
[v0.3.1]: https://github.com/kennethreitz/responder/compare/v0.3.0..v0.3.1
[v0.3.0]: https://github.com/kennethreitz/responder/compare/v0.2.3..v0.3.0
[v0.2.3]: https://github.com/kennethreitz/responder/compare/v0.2.2..v0.2.3
[v0.2.2]: https://github.com/kennethreitz/responder/compare/v0.2.1..v0.2.2
[v0.2.1]: https://github.com/kennethreitz/responder/compare/v0.2.0..v0.2.1
[v0.2.0]: https://github.com/kennethreitz/responder/compare/v0.1.6..v0.2.0
[v0.1.6]: https://github.com/kennethreitz/responder/compare/v0.1.5..v0.1.6
[v0.1.5]: https://github.com/kennethreitz/responder/compare/v0.1.4..v0.1.5
[v0.1.4]: https://github.com/kennethreitz/responder/compare/v0.1.3..v0.1.4
[v0.1.3]: https://github.com/kennethreitz/responder/compare/v0.1.2..v0.1.3
[v0.1.2]: https://github.com/kennethreitz/responder/compare/v0.1.1..v0.1.2
[v0.1.1]: https://github.com/kennethreitz/responder/compare/v0.1.0..v0.1.1
[v0.1.0]: https://github.com/kennethreitz/responder/compare/v0.0.10..v0.1.0
[v0.0.10]: https://github.com/kennethreitz/responder/compare/v0.0.9..v0.0.10
[v0.0.9]: https://github.com/kennethreitz/responder/compare/v0.0.8..v0.0.9
[v0.0.8]: https://github.com/kennethreitz/responder/compare/v0.0.7..v0.0.8
[v0.0.7]: https://github.com/kennethreitz/responder/compare/v0.0.6..v0.0.7
[v0.0.6]: https://github.com/kennethreitz/responder/compare/v0.0.5..v0.0.6
[v0.0.5]: https://github.com/kennethreitz/responder/compare/v0.0.4..v0.0.5
[v0.0.4]: https://github.com/kennethreitz/responder/compare/v0.0.3..v0.0.4
[v0.0.3]: https://github.com/kennethreitz/responder/compare/v0.0.2..v0.0.3
[v0.0.2]: https://github.com/kennethreitz/responder/compare/v0.0.1..v0.0.2
[v0.0.1]: https://github.com/kennethreitz/responder/compare/v0.0.0..v0.0.1
+178
View File
@@ -0,0 +1,178 @@
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
-24
View File
@@ -1,24 +0,0 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
waitress = "*"
werkzeug = "*"
pyyaml = "*"
requests = "*"
requests-wsgi-adapter = "*"
graphene = "*"
whitenoise = "*"
[dev-packages]
pytest = "*"
"flake8" = "*"
black = "*"
[requires]
python_version = "3.7"
[pipenv]
allow_prereleases = true
Generated
-271
View File
@@ -1,271 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "f6b7cc3cf0ac2760ea99bcb8d18c743eff418c6269da29823ccfdbdea19a8c1e"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"aniso8601": {
"hashes": [
"sha256:7849749cf00ae0680ad2bdfe4419c7a662bef19c03691a19e008c8b9a5267802",
"sha256:94f90871fcd314a458a3d4eca1c84448efbd200e86f55fe4c733c7a40149ef50"
],
"version": "==3.0.2"
},
"certifi": {
"hashes": [
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
],
"version": "==2018.8.24"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"graphene": {
"hashes": [
"sha256:b8ec446d17fa68721636eaad3d6adc1a378cb6323e219814c8f98c9928fc9642",
"sha256:faa26573b598b22ffd274e2fd7a4c52efa405dcca96e01a62239482246248aa3"
],
"index": "pypi",
"version": "==2.1.3"
},
"graphql-core": {
"hashes": [
"sha256:889e869be5574d02af77baf1f30b5db9ca2959f1c9f5be7b2863ead5a3ec6181",
"sha256:9462e22e32c7f03b667373ec0a84d95fba10e8ce2ead08f29fbddc63b671b0c1"
],
"version": "==2.1"
},
"graphql-relay": {
"hashes": [
"sha256:2716b7245d97091af21abf096fabafac576905096d21ba7118fba722596f65db"
],
"version": "==0.4.5"
},
"idna": {
"hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
],
"version": "==2.7"
},
"promise": {
"hashes": [
"sha256:2ebbfc10b7abf6354403ed785fe4f04b9dfd421eb1a474ac8d187022228332af",
"sha256:348f5f6c3edd4fd47c9cd65aed03ac1b31136d375aa63871a57d3e444c85655c"
],
"version": "==2.2.1"
},
"pyyaml": {
"hashes": [
"sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb",
"sha256:3108529b78577327d15eec243f0ff348a0640b0c3478d67ad7f5648f93bac3e2",
"sha256:3c17fb92c8ba2f525e4b5f7941d850e7a48c3a59b32d331e2502a3cdc6648e76",
"sha256:8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b",
"sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b"
],
"index": "pypi",
"version": "==4.2b4"
},
"requests": {
"hashes": [
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
],
"index": "pypi",
"version": "==2.19.1"
},
"requests-wsgi-adapter": {
"hashes": [
"sha256:7080c98ae2614b8d0b7339b611d97a535470d2fb479731f7d588d5f8108ea134"
],
"index": "pypi",
"version": "==0.4.0"
},
"rx": {
"hashes": [
"sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23",
"sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105"
],
"version": "==1.6.1"
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
],
"version": "==1.11.0"
},
"urllib3": {
"hashes": [
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
],
"markers": "python_version < '4' and python_version != '3.3.*' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.6'",
"version": "==1.23"
},
"waitress": {
"hashes": [
"sha256:40b0f297a7f3af61fbfbdc67e59090c70dc150a1601c39ecc9f5f1d283fb931b",
"sha256:d33cd3d62426c0f1b3cd84ee3d65779c7003aae3fc060dee60524d10a57f05a9"
],
"index": "pypi",
"version": "==1.1.0"
},
"werkzeug": {
"hashes": [
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
],
"index": "pypi",
"version": "==0.14.1"
},
"whitenoise": {
"hashes": [
"sha256:133a92ff0ab8fb9509f77d4f7d0de493eca19c6fea973f4195d4184f888f2e02",
"sha256:32b57d193478908a48acb66bf73e7a3c18679263e3e64bfebcfac1144a430039"
],
"index": "pypi",
"version": "==4.1"
}
},
"develop": {
"appdirs": {
"hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
],
"version": "==1.4.3"
},
"atomicwrites": {
"hashes": [
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
],
"markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.1.*'",
"version": "==1.2.1"
},
"attrs": {
"hashes": [
"sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
"sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
],
"version": "==18.2.0"
},
"black": {
"hashes": [
"sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739",
"sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"
],
"index": "pypi",
"version": "==18.9b0"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"markers": "python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.1.*' and python_version >= '2.7'",
"version": "==7.0"
},
"colorama": {
"hashes": [
"sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda",
"sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"
],
"markers": "sys_platform == 'win32'",
"version": "==0.3.9"
},
"flake8": {
"hashes": [
"sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0",
"sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"
],
"index": "pypi",
"version": "==3.5.0"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
],
"version": "==0.6.1"
},
"more-itertools": {
"hashes": [
"sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
"sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
"sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
],
"version": "==4.3.0"
},
"pluggy": {
"hashes": [
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
],
"markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.1.*'",
"version": "==0.7.1"
},
"py": {
"hashes": [
"sha256:06a30435d058473046be836d3fc4f27167fd84c45b99704f2fb5509ef61f9af1",
"sha256:50402e9d1c9005d759426988a492e0edaadb7f4e68bcddfea586bc7432d009c6"
],
"markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.1.*'",
"version": "==1.6.0"
},
"pycodestyle": {
"hashes": [
"sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766",
"sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9"
],
"version": "==2.3.1"
},
"pyflakes": {
"hashes": [
"sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f",
"sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
],
"version": "==1.6.0"
},
"pytest": {
"hashes": [
"sha256:7e258ee50338f4e46957f9e09a0f10fb1c2d05493fa901d113a8dafd0790de4e",
"sha256:9332147e9af2dcf46cd7ceb14d5acadb6564744ddff1fe8c17f0ce60ece7d9a2"
],
"index": "pypi",
"version": "==3.8.2"
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
],
"version": "==1.11.0"
},
"toml": {
"hashes": [
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
],
"version": "==0.10.0"
}
}
}
+95 -21
View File
@@ -1,35 +1,109 @@
# Responder: a Sorta Familar HTTP Framework for Python
# Responder
![](https://farm2.staticflickr.com/1937/30196007887_604e2f10d8_k_d.jpg)
A familiar HTTP Service Framework for Python, powered by [Starlette](https://www.starlette.io/).
I'm adept to keep the "for humans" tagline off this project, until it comes out of the prototyping phase. I'm building this to learn, and to have fun -- while, at the same time, trying to bring something new to the table.
```python
import responder
The Python world certianly doesn't need more web frameworks. But, it does need more creativity, so I thought I'd bring some of my ideas to the table and see what I could come up with.
api = responder.API()
# The Basic Idea
@api.route("/{greeting}")
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
The primary concept here is to bring the nicities that are brought forth from both Flask and Falcon and unify them into a single framework, along with some new ideas I have. I also wanted to take some of the API primitaves that are instilled in the Requests library and put them into a web framework. So, you'll find a lot of parallels here with Requests.
if __name__ == "__main__":
api.run()
```
## Old Ideas
$ pip install responder
- Flask-style route expression, with new capabilities -- primarily, the ability to cast a parameter to integers as well as other types that are missing from Flask, all while using Python 3.6+'s new f-string syntax.
That's it. Supports Python 3.10+.
- I love Falcon's "every request and response is passed into to each view and mutated" methodology, especially `response.media`, and have used it here. In addition to supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation and all that.
## The Basics
## New Ideas
- `resp.text` sends back text. `resp.html` sends back HTML. `resp.content` sends back bytes.
- `resp.media` sends back JSON (or YAML, with content negotiation).
- `resp.file("path.pdf")` serves a file with automatic content-type detection.
- `req.headers` is case-insensitive. `req.params` gives you query parameters.
- Both sync and async views work — the `async` is optional.
- **A built in testing client that uses the actual Requests you know and love**.
- In addition to Falcon's `on_get`, `on_post`, etc methods, Responder features an `on_request` method, which gets called on every type of request, much like Requests.
- WhiteNoise is built-in, for serving static files (this has yet to be built out, there's no templating or `static_url` yet)
- Waitress (will-be) built-in as a production web server. I would have chosen Gunicorn, but it doesn't run on Windows. Plus, Waitress serves well to protect against slowloris attacks, making nginx unneccessary in production.
- GraphQL support, via Graphene. The goal here is to eventually have an embedded version of GraphiQL exposable at any route.
## Highlights
## Future Ideas
```python
# Type-safe route parameters
@api.route("/users/{user_id:int}")
async def get_user(req, resp, *, user_id):
resp.media = {"id": user_id}
- I want to be able to "mount" any WSGI app into a sub-route.
- Cooke-based sessions are currently an afterthrought, as this is an API framework, but websites are APIs too.
- Potentially support ASGI instead of WSGI. Will the tradeoffs be worth it? This is a question to ask. Procedural code works well for 90% use cases.
# HTTP method filtering
@api.route("/items", methods=["POST"])
async def create_item(req, resp):
data = await req.media()
resp.media = {"created": data}
# The Goal
# Class-based views
@api.route("/things/{id}")
class ThingResource:
def on_get(self, req, resp, *, id):
resp.media = {"id": id}
def on_post(self, req, resp, *, id):
resp.text = "created"
The primary goal here is to learn, not to get adoption. Though, who knows how these things will pan out.
# Before-request hooks (auth, rate limiting, etc.)
@api.route(before_request=True)
def check_auth(req, resp):
if not req.headers.get("Authorization"):
resp.status_code = 401
resp.media = {"error": "unauthorized"}
# Custom error handling
@api.exception_handler(ValueError)
async def handle_error(req, resp, exc):
resp.status_code = 400
resp.media = {"error": str(exc)}
# Lifespan events
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
print("starting up")
yield
print("shutting down")
api = responder.API(lifespan=lifespan)
# GraphQL
import graphene
api.graphql("/graphql", schema=graphene.Schema(query=Query))
# WebSockets
@api.route("/ws", websocket=True)
async def websocket(ws):
await ws.accept()
while True:
name = await ws.receive_text()
await ws.send_text(f"Hello {name}!")
# Mount WSGI/ASGI apps
from flask import Flask
flask_app = Flask(__name__)
api.mount("/flask", flask_app)
# Background tasks
@api.route("/work")
def do_work(req, resp):
@api.background.task
def process():
import time; time.sleep(10)
process()
resp.media = {"status": "processing"}
```
Built-in OpenAPI docs, cookie-based sessions, gzip compression, static file serving, Jinja2 templates, and a production uvicorn server.
Route convertors: `str`, `int`, `float`, `uuid`, `path`.
## Documentation
https://responder.kennethreitz.org
-53
View File
@@ -1,53 +0,0 @@
import responder
import graphene
api = responder.API(static="static")
# api.mount('/subapp', other_wsgi_app)
@api.route("/")
def hello(req, resp):
print(resp)
# resp.status = responder.status.ok
resp.media = {"hello": "world"}
class ThingsResource:
def on_request(self, req, resp):
resp.status = responder.status.HTTP_200
resp.media = ["ylolo"]
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return "Hello " + name
schema = graphene.Schema(query=Query)
class GraphQLResource(responder.GraphQLSchema):
import graphene
def on_request(self, req, resp):
resp.status = responder.status.HTTP_200
print(schema.execute("{ hello }").data)
resp.media = ["yolo"]
# Alerntatively,
api.add_route("/2", GraphQLResource)
api.add_route("/graph", schema)
print(
api.session()
.get(
"http://app/graph?query={ hello }",
headers={"Accept": "application/x-yaml"},
# data="hello",
)
.text
)
+19
View File
@@ -0,0 +1,19 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+35
View File
@@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd
Binary file not shown.

After

Width:  |  Height:  |  Size: 762 KiB

+21
View File
@@ -0,0 +1,21 @@
<style type="text/css">
/* Make the document a little wider. */
div.document {
width: 1008px;
}
/* Better spacing around code blocks. */
div.highlight pre {
padding: 11px 14px;
}
/* Responsive layout. */
@media screen and (max-width: 1008px) {
div.sphinxsidebar {
display: none;
}
div.document {
width: 100% !important;
}
}
</style>
+14
View File
@@ -0,0 +1,14 @@
<p class="logo">
<a href="{{ pathto(master_doc) }}">
<img class="logo" src="{{ pathto('_static/responder.png', 1) }}" />
</a>
</p>
<p>
<strong>Responder</strong> — a familiar HTTP service framework for Python.
</p>
<h3>Useful Links</h3>
<ul>
<li><a href="https://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
<li><a href="https://pypi.org/project/responder/">Responder @ PyPI</a></li>
<li><a href="https://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
</ul>
+99
View File
@@ -0,0 +1,99 @@
API Reference
=============
This page documents Responder's public Python API. For usage examples
and explanations, see the :doc:`quickstart` and :doc:`tour`.
The API Class
-------------
The central object of every Responder application. It holds your routes,
middleware, templates, and configuration. Create one at the top of your
module and use it to define your entire web service.
.. module:: responder
.. autoclass:: API
:inherited-members:
Request
-------
The request object is passed into every view as the first argument. It
gives you access to everything the client sent — headers, query
parameters, the request body, cookies, and more.
Most properties are synchronous, but reading the body requires ``await``
because it involves I/O.
.. autoclass:: Request
:inherited-members:
Response
--------
The response object is passed into every view as the second argument.
Mutate it to control what gets sent back to the client — the body,
status code, headers, and cookies.
.. autoclass:: Response
:inherited-members:
Route Groups
------------
Group related routes under a shared URL prefix — useful for API versioning
and organizing large applications.
.. autoclass:: responder.api.RouteGroup
:members:
Background Queue
----------------
Run tasks in background threads without blocking the response. Available
as ``api.background``.
.. autoclass:: responder.background.BackgroundQueue
:members:
Query Dict
----------
A dictionary subclass for query string parameters with multi-value support.
.. autoclass:: responder.models.QueryDict
:members:
Rate Limiter
------------
In-memory token bucket rate limiter. Limits requests per client IP address
and returns ``429 Too Many Requests`` when exceeded.
.. autoclass:: responder.ext.ratelimit.RateLimiter
:members:
Status Code Helpers
-------------------
Convenience functions for checking which category a status code falls
into. Useful in middleware and after-request hooks.
.. autofunction:: responder.status_codes.is_100
.. autofunction:: responder.status_codes.is_200
.. autofunction:: responder.status_codes.is_300
.. autofunction:: responder.status_codes.is_400
.. autofunction:: responder.status_codes.is_500
+7
View File
@@ -0,0 +1,7 @@
# Backlog
## Future Ideas
- Consider adding `after_request` hooks (complement to `before_request`)
- Explore WebSocket before_request short-circuit support
- Add rate limiting middleware
- Consider async template rendering by default
+1
View File
@@ -0,0 +1 @@
../../CHANGELOG.md
+81
View File
@@ -0,0 +1,81 @@
Command Line Interface
======================
Responder installs a ``responder`` command that lets you launch
applications from the terminal. You can point it at a Python module,
a local file, or even a URL — and it will find your ``API`` instance
and start serving.
Launching from a Module
-----------------------
The most common way to run a Responder application in production. Use
Python's standard dotted module path::
$ responder run acme.app
This imports ``acme.app`` and looks for an attribute called ``api``
(a ``responder.API`` instance). It's the same import system Python
uses everywhere — your ``PYTHONPATH`` and virtual environment are
respected.
Launching from a File
---------------------
During development, you often have a single file you want to run::
$ responder run helloworld.py
This loads the file directly and starts the server. Quick and easy for
prototyping and single-file applications.
You can test it with a simple HTTP request::
$ curl http://127.0.0.1:5042/hello
hello, world!
Launching from a URL
--------------------
Responder can fetch and run a Python file from any URL — great for
demos, sharing examples, and running code from GitHub::
$ responder run https://github.com/kennethreitz/responder/raw/refs/heads/main/examples/helloworld.py
This also works with ``github://`` URLs and any filesystem protocol
supported by `fsspec <https://filesystem-spec.readthedocs.io/>`_::
$ responder run github://kennethreitz:responder@/examples/helloworld.py
Cloud storage is supported too — Azure Blob Storage, Google Cloud
Storage, S3, HDFS, SFTP, and more. Install ``fsspec[full]`` for all
protocols::
$ uv pip install 'fsspec[full]'
Custom Instance Names
---------------------
By default, Responder looks for an attribute called ``api``. If your
application uses a different name, specify it with a colon::
$ responder run acme.app:service
$ responder run myapp.py:application
For URLs, use a fragment::
$ responder run https://example.com/app.py#service
Building Frontend Assets
-------------------------
If your project includes a JavaScript frontend with a ``package.json``,
the ``build`` subcommand runs ``npm run build``::
$ responder build
$ responder build /path/to/frontend
+52
View File
@@ -0,0 +1,52 @@
# Sphinx configuration for Responder documentation.
import os
project = "responder"
copyright = "2018-2026, Kenneth Reitz"
author = "Kenneth Reitz"
here = os.path.abspath(os.path.dirname(__file__))
about = {}
with open(os.path.join(here, "..", "..", "responder", "__version__.py")) as f:
exec(f.read(), about)
version = about["__version__"]
release = about["__version__"]
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.viewcode",
"myst_parser",
"sphinx_copybutton",
"sphinx_design_elements",
]
templates_path = ["_templates"]
source_suffix = {".rst": "restructuredtext"}
master_doc = "index"
language = "en"
exclude_patterns = []
# Theme
html_theme = "alabaster"
html_theme_options = {
"show_powered_by": False,
"github_user": "kennethreitz",
"github_repo": "responder",
"github_banner": False,
"show_related": False,
}
html_static_path = ["_static"]
html_sidebars = {
"index": ["sidebarintro.html", "searchbox.html"],
"**": ["sidebarintro.html", "localtoc.html", "searchbox.html"],
}
# MyST
myst_heading_anchors = 3
# Copybutton
copybutton_remove_prompts = True
copybutton_prompt_text = r">>> |\.\.\. |\$ "
copybutton_prompt_is_regexp = True
+103
View File
@@ -0,0 +1,103 @@
Deployment
==========
Responder applications are standard `ASGI <https://asgi.readthedocs.io/>`_
apps. ASGI (Asynchronous Server Gateway Interface) is the modern successor
to WSGI — it supports async, WebSockets, and HTTP/2. This means you can
deploy a Responder app anywhere that runs Python, using any ASGI server.
Running Locally
---------------
During development, ``api.run()`` is all you need::
if __name__ == "__main__":
api.run()
This starts a `uvicorn <https://www.uvicorn.org/>`_ server on
``127.0.0.1:5042``. Uvicorn is a lightning-fast ASGI server built on
`uvloop <https://uvloop.readthedocs.io/>`_ — it handles thousands of
concurrent connections efficiently and protects against slowloris attacks,
making a reverse proxy like nginx optional for many deployments.
Docker
------
Docker is the most common way to package and deploy web applications.
Here's a minimal Dockerfile::
FROM python:3.13-slim
WORKDIR /app
COPY . .
RUN pip install responder
ENV PORT=80
EXPOSE 80
CMD ["python", "api.py"]
Build and run::
$ docker build -t myapi .
$ docker run -p 8000:80 myapi
The ``python:3.13-slim`` image is about 150MB — small enough for fast
deploys but includes everything you need. For even smaller images, you
can use ``python:3.13-alpine``, though some packages may need extra
build dependencies.
Cloud Platforms
---------------
Responder automatically honors the ``PORT`` environment variable. When
``PORT`` is set, the server binds to ``0.0.0.0`` on that port — this is
the convention that virtually every cloud platform uses.
This means zero configuration on:
- **Fly.io**``fly launch`` and you're done
- **Railway** — push your code, Railway sets ``PORT``
- **Render** — set start command to ``python api.py``
- **Google Cloud Run** — containerize and deploy
- **Azure Container Apps** — same pattern
- **AWS App Runner** — and here too
The pattern is always the same: deploy your code, set the start command
to ``python api.py``, and the platform handles the rest.
Uvicorn Directly
----------------
For production deployments where you want more control, bypass
``api.run()`` and use uvicorn directly::
$ uvicorn api:api --host 0.0.0.0 --port 8000 --workers 4
The ``--workers`` flag spawns multiple processes, each handling requests
independently. A good starting point is 2-4 workers per CPU core.
Uvicorn supports many options — SSL certificates, access logging, graceful
shutdown timeouts, and more. See the
`uvicorn documentation <https://www.uvicorn.org/deployment/>`_ for details.
Reverse Proxy
-------------
For high-traffic production deployments, you may want a reverse proxy like
`nginx <https://nginx.org/>`_ or `Caddy <https://caddyserver.com/>`_ in
front of your application for:
- **SSL/TLS termination** — let the proxy handle HTTPS certificates
- **Load balancing** — distribute traffic across multiple app instances
- **Static asset serving** — offload static files to the proxy
- **Rate limiting** — at the infrastructure level
Responder's ``TrustedHostMiddleware`` and ``HTTPSRedirectMiddleware`` work
correctly behind proxies that set standard forwarding headers
(``X-Forwarded-For``, ``X-Forwarded-Proto``).
That said, uvicorn is production-ready on its own. Many applications run
uvicorn directly without a reverse proxy and do just fine.
+172
View File
@@ -0,0 +1,172 @@
Configuration
=============
Every application needs different settings for different environments —
debug mode in development, real secrets in production, different database
URLs for testing. This guide covers how to manage configuration cleanly.
Environment Variables
---------------------
The simplest and most universal approach. Environment variables work
everywhere — locally, in Docker, on cloud platforms — and keep secrets
out of your source code::
import os
import responder
api = responder.API(
debug=os.getenv("DEBUG", "false").lower() == "true",
secret_key=os.environ["SECRET_KEY"],
cors=os.getenv("CORS_ENABLED", "false").lower() == "true",
)
Some variables Responder handles automatically:
- ``PORT`` — when set, the server binds to ``0.0.0.0`` on this port
Set variables in your shell::
$ export SECRET_KEY="your-secret-here"
$ export DEBUG=true
$ python app.py
Or in a ``.env`` file (don't commit this to git)::
SECRET_KEY=your-secret-here
DEBUG=true
Using .env Files
----------------
For local development, a ``.env`` file is convenient. Install
``python-dotenv`` and load it at the top of your app::
$ uv pip install python-dotenv
::
from dotenv import load_dotenv
load_dotenv()
import os
import responder
api = responder.API(
secret_key=os.environ["SECRET_KEY"],
)
Add ``.env`` to your ``.gitignore`` — never commit secrets.
Configuration Class Pattern
----------------------------
For larger applications, a configuration class keeps things organized::
import os
class Config:
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret")
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///dev.db")
CORS_ORIGINS = os.environ.get("CORS_ORIGINS", "").split(",")
config = Config()
api = responder.API(
debug=config.DEBUG,
secret_key=config.SECRET_KEY,
cors=bool(config.CORS_ORIGINS[0]),
cors_params={"allow_origins": config.CORS_ORIGINS},
)
This makes it easy to see all your settings in one place.
Secret Key
----------
The ``secret_key`` is used to sign session cookies. If someone knows your
secret key, they can forge session data and impersonate any user.
Rules:
- **Never use the default** in production
- **Generate a random key**: ``python -c "import secrets; print(secrets.token_hex(32))"``
- **Store it in an environment variable**, not in code
- **Rotate it** if it's ever compromised (this invalidates all sessions)
::
api = responder.API(secret_key=os.environ["SECRET_KEY"])
Debug Mode
----------
Debug mode controls error page behavior:
- **On** (``debug=True``): detailed error pages with tracebacks. Never
use this in production — it exposes your source code.
- **Off** (``debug=False``): generic error pages. This is the default.
::
api = responder.API(debug=True) # development only
A common pattern is to read it from the environment::
api = responder.API(debug=os.getenv("DEBUG") == "true")
Allowed Hosts
-------------
In production, always set ``allowed_hosts`` to prevent Host header
attacks. This should match the domain names your application serves::
api = responder.API(
allowed_hosts=["example.com", "www.example.com"],
)
In development, you can use ``["*"]`` (the default) or specific local
addresses::
api = responder.API(allowed_hosts=["localhost", "127.0.0.1"])
Putting It All Together
-----------------------
A production-ready configuration setup::
import os
from dotenv import load_dotenv
load_dotenv()
import responder
api = responder.API(
debug=os.getenv("DEBUG", "false") == "true",
secret_key=os.environ["SECRET_KEY"],
allowed_hosts=os.getenv("ALLOWED_HOSTS", "*").split(","),
cors=bool(os.getenv("CORS_ORIGINS")),
cors_params={
"allow_origins": os.getenv("CORS_ORIGINS", "").split(","),
"allow_methods": ["GET", "POST", "PUT", "DELETE"],
},
)
With a ``.env`` file for local development::
SECRET_KEY=dev-secret-do-not-use-in-prod
DEBUG=true
ALLOWED_HOSTS=localhost,127.0.0.1
CORS_ORIGINS=http://localhost:3000
And environment variables set properly in production (via your cloud
platform's dashboard, Docker secrets, or a secrets manager).
+130
View File
@@ -0,0 +1,130 @@
Responder
=========
A familiar HTTP Service Framework for Python.
.. code:: python
import responder
api = responder.API()
@api.route("/{greeting}")
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
if __name__ == '__main__':
api.run()
Powered by `Starlette`_, `uvicorn`_, and good intentions. The ``async`` is optional.
The Idea
--------
If you've ever used `Flask`_, the routing will look familiar. If you've
used `Falcon`_, the request/response pattern will click immediately. And
if you've used `Requests`_ — well, you'll feel right at home.
Responder takes these ideas and brings them together. Every view receives
a request and a response. You read from one and write to the other. No
return values, no special response classes, no boilerplate.
- ``resp.text`` sends text. ``resp.html`` sends HTML. ``resp.media`` sends JSON.
- ``resp.file("path")`` serves a file. ``resp.content`` sends raw bytes.
- ``req.headers`` is case-insensitive. ``req.params`` holds query parameters.
- ``resp.status_code``, ``req.method``, ``req.url`` — the familiar ones.
Set ``resp.media`` to a dict and the right thing happens. If the client
asks for YAML, it gets YAML. Content negotiation is automatic.
Responder and `FastAPI`_ are siblings — both built on Starlette, both
born around the same time, both part of the push that made ASGI the
future of Python web services. FastAPI went deep on type annotations
and automatic validation. Responder went for simplicity and a mutable
request/response pattern. Both projects are better for the other
existing. Use whichever feels right.
This is a passion project. It exists because building a web framework
from scratch is one of the best ways to understand how the web works.
It's a great fit for personal projects, prototyping, teaching, research,
and anyone who values a clean API over a sprawling ecosystem. If you
need battle-tested infrastructure at scale, FastAPI and Django will
serve you well. If you want something small, expressive, and fun to
work with — welcome.
What You Get
------------
One ``pip install``, batteries included:
- Mount Flask, Django, or any WSGI/ASGI app at a subroute.
- Gzip compression, HSTS, CORS, and trusted host validation.
- Before-request hooks that can short-circuit for auth guards.
- A test client for fast, in-process testing with pytest.
- Route parameters with f-string syntax and type convertors.
- Lifespan context managers for startup and shutdown logic.
- Custom exception handlers for clean error responses.
- `GraphQL`_ with Graphene and a built-in GraphiQL IDE.
- File serving with automatic content-type detection.
- Sync and async views — ``async`` is always optional.
- Class-based views with ``on_get``, ``on_post``, ``on_request``.
- A pleasant API with a single import statement.
- OpenAPI schema generation with Swagger UI.
- A production `uvicorn`_ server, ready to deploy.
- HTTP method filtering for REST APIs.
- Signed cookie-based sessions.
- Background tasks in a thread pool.
- WebSocket support.
Installation
------------
.. code-block:: shell
$ uv pip install responder
Python 3.10 and above. That's it.
.. toctree::
:maxdepth: 2
:caption: User Guide
quickstart
tour
deployment
testing
api
cli
.. toctree::
:maxdepth: 2
:caption: Tutorials
tutorial-rest
tutorial-sqlalchemy
tutorial-auth
tutorial-websockets
tutorial-middleware
tutorial-flask
guide-config
.. toctree::
:maxdepth: 1
:caption: Project
changes
Sandbox <sandbox>
backlog
.. _Starlette: https://www.starlette.io/
.. _uvicorn: https://www.uvicorn.org/
.. _Flask: https://flask.palletsprojects.com/
.. _Falcon: https://falconframework.org/
.. _FastAPI: https://fastapi.tiangolo.com/
.. _GraphQL: https://graphql.org/
.. _Requests: https://requests.readthedocs.io/
+378
View File
@@ -0,0 +1,378 @@
Quick Start
===========
This guide will walk you through the basics of building a web service with
Responder. By the end, you'll understand how HTTP requests and responses
work, how to define routes, read data from clients, send data back, render
HTML templates, and process work in the background.
Create a Web Service
--------------------
Every web application starts with a single object — the application
instance. In Responder, this is the ``API`` class. It holds your routes,
middleware, templates, and configuration. Think of it as the central
nervous system of your web service::
import responder
api = responder.API()
That's it. One import, one line. You now have a fully functional ASGI
application with gzip compression, static file serving, session support,
and a production-ready server — all wired up and ready to go.
Hello World
-----------
A web service isn't very useful until it can respond to requests. In HTTP,
a *route* maps a URL path to a function that handles it. When a client
(like a browser or ``curl``) sends a request to that path, your function
runs and produces a response.
Here's the simplest possible route::
@api.route("/")
def hello_world(req, resp):
resp.text = "hello, world!"
Two things to notice:
1. Every view function receives two arguments: ``req`` (the incoming
request) and ``resp`` (the outgoing response).
2. You don't return anything. Instead, you *mutate* the response object
directly. This is a deliberate design choice — it keeps the API
consistent whether you're setting text, JSON, headers, cookies, or
status codes.
Run the Server
--------------
Start your web service with a single call::
api.run()
This spins up a production-grade `uvicorn <https://www.uvicorn.org/>`_
server on port ``5042``, ready for incoming HTTP requests. Open
``http://localhost:5042`` in your browser and you'll see your hello world
response.
You can customize the port with ``api.run(port=8000)``. The ``PORT``
environment variable is also honored automatically — when set, Responder
binds to ``0.0.0.0`` on that port, which is what cloud platforms expect.
.. note::
Both sync and async views are supported. The ``async`` keyword is always
optional — use it when you need to ``await`` something, like reading a
request body or querying a database.
Route Parameters
----------------
Static URLs like ``/about`` are useful, but most applications need dynamic
routes — URLs that contain variable data, like a user ID or a product slug.
In Responder, you declare route parameters using Python's f-string syntax::
@api.route("/hello/{who}")
def hello_to(req, resp, *, who):
resp.text = f"hello, {who}!"
A ``GET`` request to ``/hello/world`` will respond with ``hello, world!``.
A request to ``/hello/guido`` will respond with ``hello, guido!``.
Route parameters are passed as *keyword-only* arguments (after the ``*``
in the function signature). This is a Python feature that makes the
interface explicit — you always know which arguments come from the URL.
Type Convertors
^^^^^^^^^^^^^^^
By default, route parameters are strings. But often you want them as
integers, UUIDs, or other types. Responder can convert them automatically
using type annotations in the route pattern::
@api.route("/add/{a:int}/{b:int}")
async def add(req, resp, *, a, b):
resp.text = f"{a} + {b} = {a + b}"
Here, ``a`` and ``b`` will arrive as Python ``int`` objects, not strings.
If someone requests ``/add/3/hello``, they'll get a 404 — the route won't
match because ``hello`` isn't a valid integer.
Supported types:
- ``str`` — matches any string without slashes (this is the default)
- ``int`` — matches digits and converts to ``int``
- ``float`` — matches decimal numbers and converts to ``float``
- ``uuid`` — matches UUID strings like ``550e8400-e29b-41d4-a716-446655440000``
- ``path`` — matches any string *including* slashes, useful for file paths
like ``/files/{filepath:path}``
Sending Responses
-----------------
When an HTTP server receives a request, it must send back a response. Every
HTTP response has three parts: a status code (like ``200 OK`` or ``404 Not
Found``), headers (metadata like ``Content-Type``), and a body (the actual
data).
Responder lets you set all three by mutating the response object.
**Text and HTML** — the simplest response types. ``resp.text`` sets the
``Content-Type`` to ``text/plain``, while ``resp.html`` sets it to
``text/html``::
resp.text = "plain text response"
resp.html = "<h1>HTML response</h1>"
**JSON** — the lingua franca of web APIs. Set ``resp.media`` to any
JSON-serializable Python object — a dict, a list, whatever — and Responder
will serialize it to JSON and set the right headers::
@api.route("/hello/{who}/json")
def hello_json(req, resp, *, who):
resp.media = {"hello": who}
If the client sends an ``Accept: application/x-yaml`` header, the same data
will be returned as YAML instead. This is called *content negotiation*
the server and client agree on a format. It happens automatically.
**Files** — serve a file from disk. Responder uses Python's ``mimetypes``
module to figure out the ``Content-Type`` from the file extension::
resp.file("reports/annual.pdf")
**Raw bytes** — for binary data like images or protocol buffers::
resp.content = b"\x89PNG\r\n..."
**Status codes** — HTTP status codes tell the client what happened. ``200``
means success, ``201`` means something was created, ``404`` means not found,
``500`` means the server broke. Set it directly::
resp.status_code = 201
**Headers** — HTTP headers carry metadata. Common ones include
``Content-Type``, ``Cache-Control``, ``Authorization``, and custom
application headers::
resp.headers["X-Custom"] = "value"
**Redirects** — tell the client to go somewhere else::
api.redirect(resp, location="/new-url")
This sends a ``301 Moved Permanently`` response by default. The client's
browser will automatically follow the redirect.
Reading Requests
----------------
The other half of HTTP is the request — the data the client sends to your
server. This includes the HTTP method (GET, POST, PUT, DELETE), the URL,
headers, query parameters, cookies, and optionally a body.
Responder wraps all of this in the ``req`` object.
**Method and URL** — every HTTP request has a method (what the client wants
to do) and a URL (what resource it's about)::
req.method # "get", "post", etc. (lowercase)
req.full_url # "http://example.com/path?q=1"
req.url # parsed URL object
**Headers** — HTTP headers carry metadata from the client, like what
content types it accepts, authentication tokens, and more. Responder's
headers dict is case-insensitive, because the HTTP spec says header names
are case-insensitive::
req.headers["Content-Type"]
req.headers["content-type"] # same thing
**Query parameters** — the part of the URL after the ``?``. These are
commonly used for search, filtering, and pagination::
# GET /search?q=python&page=2
req.params["q"] # "python"
req.params["page"] # "2"
Note that query parameters are always strings. If you need an integer,
you'll need to convert it yourself: ``int(req.params["page"])``.
**Path parameters** — the dynamic parts of the URL that matched your route
pattern. These are also available on the request object, which is useful
in before-request hooks where they aren't passed as function arguments::
req.path_params["user_id"] # same as the keyword argument
**Request body** — for POST, PUT, and PATCH requests, the client sends
data in the body. Since reading the body is an I/O operation, you need to
``await`` it::
# JSON body (the most common format for APIs)
data = await req.media()
# Form data (from HTML forms)
data = await req.media("form")
# File uploads (multipart)
files = await req.media("files")
# Raw bytes
body = await req.content
# Raw text
text = await req.text
**Other useful properties**::
req.is_json # True if the content type is JSON
req.cookies # dict of cookies sent by the client
req.session # session data (a signed, server-side dict)
req.client # (host, port) tuple — the client's IP address
req.is_secure # True if the request came over HTTPS
Rendering Templates
-------------------
While APIs typically return JSON, many web applications need to render
HTML pages. Responder includes built-in support for
`Jinja2 <https://jinja.palletsprojects.com/>`_, one of the most popular
templating engines in the Python ecosystem.
Templates let you write HTML with placeholders that get filled in with
dynamic data. This keeps your presentation logic (HTML) separate from
your application logic (Python) — a pattern called
*separation of concerns*.
The simplest way to render a template is ``api.template()``. Templates
are loaded from the ``templates/`` directory by default::
@api.route("/hello/{name}/html")
def hello_html(req, resp, *, name):
resp.html = api.template("hello.html", name=name)
The template file ``templates/hello.html`` might look like::
<h1>Hello, {{ name }}!</h1>
The ``{{ name }}`` part is a Jinja2 expression — it gets replaced with
the value you passed in.
You can also use the ``Templates`` class directly for more control over
the template directory and configuration::
from responder.templates import Templates
templates = Templates(directory="my_templates")
@api.route("/page")
def page(req, resp):
resp.html = templates.render("page.html", title="Hello")
For applications that need non-blocking template rendering (rare, but
useful under extreme load), async rendering is supported::
templates = Templates(directory="templates", enable_async=True)
resp.html = await templates.render_async("page.html", title="Hello")
And for quick one-off templates, you can render a string directly without
a file::
resp.html = api.template_string("Hello, {{ name }}!", name="world")
Background Tasks
----------------
Sometimes you want to accept a request, respond immediately, and do the
actual processing later. This is a common pattern for operations that take
a long time — sending emails, processing images, updating caches, or
calling slow external APIs.
Responder makes this easy with background tasks. Decorate any function
with ``@api.background.task`` and it will run in a thread pool, separate
from the request/response cycle::
@api.route("/incoming")
async def receive_incoming(req, resp):
data = await req.media()
@api.background.task
def process_data(data):
"""This runs in a background thread."""
import time
time.sleep(10) # simulate heavy work
process_data(data)
# This response is sent immediately, while process_data
# continues running in the background.
resp.media = {"status": "accepted"}
The client gets an instant response — the heavy lifting happens after.
This is the same pattern used by task queues like Celery, but much simpler
for lightweight use cases where you don't need a full message broker.
.. note::
Background tasks run in threads, not processes. They share memory with
your application, which makes them fast to start but means CPU-intensive
work will block the event loop. For heavy computation, consider a proper
task queue.
Putting It All Together
-----------------------
Here's a complete, working Responder application that combines everything
from this guide::
import responder
api = responder.API()
@api.route("/")
def index(req, resp):
resp.text = "Welcome to the API"
@api.route("/hello/{name}")
def greet(req, resp, *, name):
resp.media = {"message": f"hello, {name}!"}
@api.route("/add/{a:int}/{b:int}")
def add(req, resp, *, a, b):
resp.media = {"result": a + b}
@api.route("/echo", methods=["POST"])
async def echo(req, resp):
data = await req.media()
resp.media = {"received": data}
if __name__ == "__main__":
api.run()
Save this as ``app.py``, run it with ``python app.py``, and try::
$ curl http://localhost:5042/
$ curl http://localhost:5042/hello/world
$ curl http://localhost:5042/add/3/4
$ curl -X POST http://localhost:5042/echo \
-H "Content-Type: application/json" -d '{"key": "value"}'
From here, explore the :doc:`tour` for the full range of features, or
jump into the tutorials:
- :doc:`tutorial-rest` — build a full CRUD API with validation
- :doc:`tutorial-sqlalchemy` — connect to a database
- :doc:`tutorial-auth` — add authentication
+36
View File
@@ -0,0 +1,36 @@
(sandbox)=
# Development Sandbox
## Setup
Set up a development sandbox.
Acquire sources and create virtualenv.
```shell
git clone https://github.com/kennethreitz/responder.git
cd responder
uv venv
```
Install project in editable mode, including
all development tools.
```shell
uv pip install --upgrade --editable '.[develop,docs,release,test]'
```
## Operations
Run tests.
```shell
source .venv/bin/activate
pytest
```
Format code.
```shell
ruff format .
ruff check --fix .
```
Documentation authoring.
```shell
sphinx-autobuild --open-browser --watch docs/source docs/source docs/build
```
+286
View File
@@ -0,0 +1,286 @@
Testing
=======
Responder includes a built-in test client powered by Starlette's
``TestClient``. You don't need to start a server — tests run in-process,
making them fast and reliable. There's no separate test server to manage,
no ports to allocate, and no race conditions to worry about. Just import
your app and start making requests.
Getting Started
---------------
Given a simple application in ``api.py``::
import responder
api = responder.API()
@api.route("/")
def hello(req, resp):
resp.text = "hello, world!"
if __name__ == "__main__":
api.run()
You can test it with pytest. Every Responder ``API`` instance has a
``requests`` property that gives you a test client — use it exactly like
you'd use ``requests`` or ``httpx``::
# test_api.py
import api as service
def test_hello():
r = service.api.requests.get("/")
assert r.text == "hello, world!"
Run your tests::
$ pytest
That's really all there is to it. No configuration, no test server setup.
Using Fixtures
--------------
For larger test suites, pytest fixtures keep things organized. Create a
fixture that returns your API instance, and every test gets a fresh
reference to it::
import pytest
import api as service
@pytest.fixture
def api():
return service.api
def test_hello(api):
r = api.requests.get("/")
assert r.text == "hello, world!"
def test_json(api):
@api.route("/data")
def data(req, resp):
resp.media = {"key": "value"}
r = api.requests.get(api.url_for(data))
assert r.json() == {"key": "value"}
The ``api.url_for()`` method generates a URL for a given route endpoint,
so you don't have to hard-code paths in your tests. If you rename a route
later, your tests won't break.
Testing JSON APIs
-----------------
Most APIs send and receive JSON. The test client makes this natural — pass
``json=`` to send a JSON body, and call ``.json()`` on the response to
parse it::
def test_create_item(api):
@api.route("/items")
async def create(req, resp):
data = await req.media()
resp.media = {"created": data}
resp.status_code = 201
r = api.requests.post(api.url_for(create), json={"name": "widget"})
assert r.status_code == 201
assert r.json() == {"created": {"name": "widget"}}
You can also test content negotiation by setting the ``Accept`` header::
r = api.requests.get("/data", headers={"Accept": "application/x-yaml"})
assert "key: value" in r.text
Testing Request Validation
--------------------------
If you're using Pydantic models for request validation, you can test
that invalid inputs are properly rejected::
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
def test_validation(api):
@api.route("/items", methods=["POST"], request_model=Item)
async def create(req, resp):
data = await req.media()
resp.media = data
# Valid request
r = api.requests.post("/items", json={"name": "thing", "price": 9.99})
assert r.status_code == 200
# Missing required field
r = api.requests.post("/items", json={"name": "thing"})
assert r.status_code == 422
assert "errors" in r.json()
Testing File Uploads
--------------------
File uploads use the ``files`` parameter, just like the ``requests``
library. Each file is a tuple of ``(filename, content, content_type)``::
def test_upload(api):
@api.route("/upload")
async def upload(req, resp):
files = await req.media("files")
resp.media = {"received": list(files.keys())}
files = {"doc": ("report.pdf", b"content", "application/pdf")}
r = api.requests.post(api.url_for(upload), files=files)
assert r.json() == {"received": ["doc"]}
Testing Headers and Cookies
----------------------------
Check response headers and cookies just like you would with any HTTP
client::
def test_headers(api):
@api.route("/")
def view(req, resp):
resp.headers["X-Custom"] = "hello"
resp.cookies["session"] = "abc123"
r = api.requests.get("/")
assert r.headers["X-Custom"] == "hello"
assert "session" in r.cookies
Testing WebSockets
------------------
WebSocket tests use Starlette's ``TestClient`` directly, since WebSocket
connections require a different protocol. The ``websocket_connect`` context
manager gives you a connection you can send and receive on::
from starlette.testclient import TestClient
def test_websocket(api):
@api.route("/ws", websocket=True)
async def ws(ws):
await ws.accept()
name = await ws.receive_text()
await ws.send_text(f"hello, {name}!")
await ws.close()
client = TestClient(api)
with client.websocket_connect("/ws") as ws:
ws.send_text("world")
assert ws.receive_text() == "hello, world!"
Testing Error Handling
----------------------
By default, the test client raises exceptions from your route handlers,
which is usually what you want — it makes bugs obvious. But when you're
testing error handling specifically, you want to see the error response
instead. Disable exception propagation with ``raise_server_exceptions``::
from starlette.testclient import TestClient
def test_500(api):
@api.route("/fail")
def fail(req, resp):
raise ValueError("something broke")
client = TestClient(api, raise_server_exceptions=False)
r = client.get(api.url_for(fail))
assert r.status_code == 500
If you've registered a custom exception handler, you can test that too::
def test_custom_error(api):
@api.exception_handler(ValueError)
async def handle(req, resp, exc):
resp.status_code = 400
resp.media = {"error": str(exc)}
@api.route("/fail")
def fail(req, resp):
raise ValueError("bad input")
client = TestClient(api, raise_server_exceptions=False)
r = client.get(api.url_for(fail))
assert r.status_code == 400
assert r.json() == {"error": "bad input"}
Testing Lifespan Events
-----------------------
If your app uses startup and shutdown events (for database connections,
caches, etc.), you need the test client to trigger them. Wrap the client
in a ``with`` block — startup runs on enter, shutdown runs on exit::
def test_with_lifespan(api):
started = {"value": False}
@api.on_event("startup")
async def on_startup():
started["value"] = True
@api.route("/")
def check(req, resp):
resp.media = {"started": started["value"]}
with api.requests as session:
r = session.get("http://;/")
assert r.json() == {"started": True}
Without the ``with`` block, lifespan events won't fire, which can lead to
confusing test failures if your routes depend on startup initialization.
Testing Before and After Hooks
------------------------------
Before-request and after-request hooks run automatically during tests,
just like in production. You can verify their effects on the response::
def test_hooks(api):
@api.route(before_request=True)
def add_version(req, resp):
resp.headers["X-Version"] = "3.2"
@api.after_request()
def add_timing(req, resp):
resp.headers["X-Served-By"] = "responder"
@api.route("/")
def view(req, resp):
resp.text = "ok"
r = api.requests.get("/")
assert r.headers["X-Version"] == "3.2"
assert r.headers["X-Served-By"] == "responder"
Tips
----
- **Keep tests fast.** The in-process test client is already fast — no
network overhead. Avoid ``time.sleep()`` in tests.
- **One API per test** when testing configuration. If you need a specific
``API()`` configuration (like ``cors=True``), create a new instance
in the test rather than sharing the fixture.
- Use ``api.url_for()`` instead of hard-coded paths. It's a small
thing, but it makes refactoring painless.
- **Test the contract, not the implementation.** Assert on status codes,
response bodies, and headers — not on internal state.
+613
View File
@@ -0,0 +1,613 @@
Feature Tour
============
This section walks through Responder's features in depth. Each section
explains the concept, shows working code, and explains the design choices
behind it. If you're new to web development, this is a good place to learn
how modern web frameworks work under the hood.
Method Filtering
----------------
HTTP defines several *methods* (also called verbs) that describe what a
client wants to do with a resource. The most common are:
- ``GET`` — retrieve data
- ``POST`` — create something new
- ``PUT`` — replace something entirely
- ``PATCH`` — update part of something
- ``DELETE`` — remove something
By default, a Responder route matches all methods. This is fine for simple
endpoints, but REST APIs typically map different methods to different
operations. Use the ``methods`` parameter to restrict a route::
@api.route("/items", methods=["GET"])
def list_items(req, resp):
resp.media = {"items": []}
@api.route("/items", methods=["POST"], check_existing=False)
async def create_item(req, resp):
data = await req.media()
resp.media = {"created": data}
Note the ``check_existing=False`` — Responder normally prevents you from
registering two routes with the same path (to catch typos). When you
intentionally want multiple handlers for the same path with different
methods, you need to opt in.
Class-Based Views
-----------------
Function-based views are great for simple endpoints, but sometimes you want
to group related HTTP methods together into a single resource. This is
where class-based views come in — a pattern popularized by
`Falcon <https://falconframework.org/>`_.
Responder dispatches to the appropriate method handler based on the HTTP
method::
@api.route("/{greeting}")
class GreetingResource:
def on_get(self, req, resp, *, greeting):
resp.text = f"{greeting}, world!"
def on_post(self, req, resp, *, greeting):
resp.media = {"received": greeting}
def on_request(self, req, resp, *, greeting):
"""Called on EVERY request, before the method-specific handler."""
resp.headers["X-Greeting"] = greeting
The ``on_request`` method is called for all HTTP methods, much like
middleware scoped to a single route. Method-specific handlers (``on_get``,
``on_post``, ``on_put``, ``on_delete``, etc.) are called after.
No inheritance required — just define a class with the right method names.
This is simpler than Django's ``View`` classes and more Pythonic than
framework-specific base classes.
Lifespan Events
---------------
Real applications need to set up resources when they start (database
connection pools, ML models, caches) and tear them down when they stop.
This is called the application *lifespan*.
The modern approach is the *context manager* pattern, where startup and
shutdown are two halves of the same block::
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
# Startup — runs before the first request
print("connecting to database...")
yield
# Shutdown — runs after the server stops
print("closing connections...")
api = responder.API(lifespan=lifespan)
Everything before ``yield`` runs at startup. Everything after runs at
shutdown. If startup fails, the server won't start. If shutdown raises,
it's logged but the server still exits.
The traditional event decorator style also works::
@api.on_event("startup")
async def startup():
print("starting up")
@api.on_event("shutdown")
async def shutdown():
print("shutting down")
The context manager is preferred for new code — it keeps related startup
and shutdown logic together and makes resource cleanup more explicit.
Serving Files
-------------
Web applications often need to serve files — downloads, reports, images.
Responder makes this simple with ``resp.file()``, which reads a file from
disk and sets the ``Content-Type`` header automatically using Python's
``mimetypes`` module::
@api.route("/download")
def download(req, resp):
resp.file("reports/annual.pdf")
You can override the content type if the automatic detection isn't right::
@api.route("/image")
def image(req, resp):
resp.file("photos/cat.jpg", content_type="image/jpeg")
For large files, use ``resp.stream_file()`` to avoid loading the entire
file into memory. This streams the file in chunks::
@api.route("/export")
def export(req, resp):
resp.stream_file("data/export.csv")
Custom Error Handling
---------------------
In production, you don't want your users to see raw Python tracebacks.
Responder lets you register custom handlers for specific exception types,
so you can return clean, structured error responses::
@api.exception_handler(ValueError)
async def handle_value_error(req, resp, exc):
resp.status_code = 400
resp.media = {"error": str(exc)}
Now, any route that raises a ``ValueError`` will return a clean JSON
response with a 400 status code instead of a generic 500 error page.
This is a common pattern in API development — you define your own exception
classes for different error conditions, register handlers for each, and
your API always returns consistent, machine-readable error responses.
Before-Request Hooks
--------------------
Sometimes you need to run the same code before every request —
authentication checks, request logging, adding common headers, or setting
up per-request state. Before-request hooks let you do this without
duplicating code in every route::
@api.route(before_request=True)
def add_headers(req, resp):
resp.headers["X-API-Version"] = "3.2"
**Short-circuiting** is the really powerful part. If your hook sets
``resp.status_code``, the route handler is skipped entirely and the
response is sent immediately. This is the pattern for authentication::
@api.route(before_request=True)
def auth_check(req, resp):
if "Authorization" not in req.headers:
resp.status_code = 401
resp.media = {"error": "unauthorized"}
If the ``Authorization`` header is missing, the client gets a 401 response
and the actual route handler never runs. This is cleaner than adding
auth checks to every individual route.
After-Request Hooks
-------------------
The complement to before-request hooks. After-request hooks run after the
route handler completes but before the response is sent. They're useful
for logging, adding response headers, or any post-processing::
@api.after_request()
def log_response(req, resp):
print(f"{req.method} {req.full_url} -> {resp.status_code}")
@api.after_request()
async def add_timing(req, resp):
resp.headers["X-Served-By"] = "responder"
WebSocket Support
-----------------
HTTP is a request-response protocol — the client asks, the server answers.
But some applications need real-time, bidirectional communication: chat
apps, live dashboards, multiplayer games, collaborative editors.
`WebSockets <https://en.wikipedia.org/wiki/WebSocket>`_ solve this by
upgrading an HTTP connection into a persistent, full-duplex channel where
both sides can send messages at any time::
@api.route("/ws", websocket=True)
async def websocket(ws):
await ws.accept()
while True:
name = await ws.receive_text()
await ws.send_text(f"Hello {name}!")
await ws.close()
You can send and receive in multiple formats:
- ``send_text`` / ``receive_text`` — plain text strings
- ``send_json`` / ``receive_json`` — JSON objects (auto-serialized)
- ``send_bytes`` / ``receive_bytes`` — raw binary data
WebSocket routes are marked with ``websocket=True`` in the route decorator.
They receive a ``ws`` object instead of ``req`` and ``resp``.
Server-Sent Events (SSE)
-------------------------
SSE is a simpler alternative to WebSockets for *one-way* real-time
communication — the server pushes events to the client, but the client
can't send messages back. This is perfect for live feeds, progress bars,
notification streams, and AI response streaming.
Unlike WebSockets, SSE works over plain HTTP, is automatically reconnected
by the browser, and doesn't require any special client-side libraries::
@api.route("/events")
async def events(req, resp):
@resp.sse
async def stream():
for i in range(10):
yield {"data": f"message {i}"}
On the client side, you consume SSE events with JavaScript's built-in
``EventSource`` API::
const source = new EventSource("/events");
source.onmessage = (event) => {
console.log(event.data);
};
Each yielded value can be a string (treated as data) or a dict with the
standard SSE fields::
yield {"event": "update", "data": "hello", "id": "1", "retry": "5000"}
yield "simple string message"
GraphQL
-------
`GraphQL <https://graphql.org/>`_ is a query language for APIs that lets
clients request exactly the data they need — no more, no less. Instead of
multiple REST endpoints, you define a schema and let clients query it.
Responder includes built-in GraphQL support via
`Graphene <https://graphene-python.org/>`_. Set up a full GraphQL endpoint
with a single method call::
import graphene
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return f"Hello {name}"
api.graphql("/graphql", schema=graphene.Schema(query=Query))
Visiting ``/graphql`` in a browser renders the
`GraphiQL <https://github.com/graphql/graphiql>`_ interactive IDE, where
you can explore your schema, write queries, and see results in real-time.
Programmatic clients can POST JSON queries to the same endpoint.
You can access the Responder request and response objects in your resolvers
through ``info.context["request"]`` and ``info.context["response"]``.
OpenAPI Documentation
---------------------
`OpenAPI <https://www.openapis.org/>`_ (formerly Swagger) is the industry
standard for describing REST APIs. An OpenAPI specification lets you
auto-generate interactive documentation, client libraries, and validation
logic.
Responder generates OpenAPI specs from your code::
api = responder.API(
title="Pet Store",
version="1.0",
openapi="3.0.2",
docs_route="/docs",
)
This gives you:
- An OpenAPI schema at ``/schema.yml``
- Interactive Swagger UI documentation at ``/docs``
There are three ways to document your endpoints.
**Pydantic models** — the recommended approach. Use ``request_model`` and
``response_model`` to annotate your routes, and Responder generates the
schema automatically. When ``request_model`` is set, request bodies are
also validated automatically — invalid inputs get a ``422`` response with
detailed error messages::
from pydantic import BaseModel
class PetIn(BaseModel):
name: str
age: int = 0
class PetOut(BaseModel):
id: int
name: str
age: int
@api.route("/pets", methods=["POST"],
request_model=PetIn, response_model=PetOut)
async def create_pet(req, resp):
data = await req.media()
resp.media = {"id": 1, **data}
When ``response_model`` is set, the response is serialized through the
model — extra fields are stripped and types are enforced.
**YAML docstrings** — for full control, embed OpenAPI YAML in the
docstring::
@api.route("/pets")
def list_pets(req, resp):
"""A list of pets.
---
get:
description: Get all pets
responses:
200:
description: A list of pets
"""
resp.media = [{"name": "Fido"}]
**Marshmallow schemas** — if you're already using marshmallow::
from marshmallow import Schema, fields
@api.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
All three approaches can be mixed in the same API. You can choose from
multiple documentation themes: ``swagger_ui`` (default), ``redoc``,
``rapidoc``, or ``elements``.
Route Groups
------------
As your application grows, you'll want to organize routes logically.
Route groups let you share a URL prefix across related endpoints — a
common pattern for API versioning::
v1 = api.group("/v1")
@v1.route("/users")
def list_users(req, resp):
resp.media = []
@v1.route("/users/{user_id:int}")
def get_user(req, resp, *, user_id):
resp.media = {"id": user_id}
v2 = api.group("/v2")
@v2.route("/users")
def list_users_v2(req, resp):
resp.media = {"users": [], "total": 0}
This keeps your code organized without affecting the routing logic.
Mounting Other Apps
-------------------
Responder can mount any WSGI or ASGI application at a subroute. This is
incredibly useful for gradual migrations — you can run Flask and Responder
side by side, moving routes over one at a time::
from flask import Flask
flask_app = Flask(__name__)
@flask_app.route("/")
def hello():
return "Hello from Flask!"
api.mount("/flask", flask_app)
Requests to ``/flask/`` will be handled by Flask. Everything else goes
through Responder. Both WSGI and ASGI apps are supported — Responder
wraps WSGI apps in an ASGI adapter automatically.
You can also mount `marimo <https://marimo.io/>`_ notebooks as
interactive dashboards within your API::
import marimo
server = (
marimo.create_asgi_app()
.with_app(path="", root="./notebooks/dashboard.py")
.with_app(path="/analysis", root="./notebooks/analysis.py")
)
api.mount("/notebooks", server.build())
Notebooks are served at ``/notebooks/`` and ``/notebooks/analysis``,
with full interactivity — reactive cells, widgets, plots, and all.
Cookies
-------
`Cookies <https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies>`_ are
small pieces of data that the server asks the browser to store and send
back with every subsequent request. They're the foundation of sessions,
authentication tokens, and user preferences on the web.
Reading and writing cookies is straightforward::
# Read cookies from the request
session_id = req.cookies.get("session_id")
# Set a cookie on the response
resp.cookies["hello"] = "world"
For production use, you'll want to set security directives. The
``httponly`` flag prevents JavaScript from reading the cookie (defending
against XSS attacks), and ``secure`` ensures it's only sent over HTTPS::
resp.set_cookie(
"token",
value="abc123",
max_age=3600, # expires in 1 hour
secure=True, # HTTPS only
httponly=True, # no JavaScript access
path="/",
)
Cookie-Based Sessions
---------------------
Sessions let you store per-user data across multiple requests. Responder's
built-in sessions are cookie-based — the session data is serialized, signed
with your secret key, and stored in a cookie. The signature prevents
tampering: if someone modifies the cookie, the signature won't match and
the data will be rejected::
@api.route("/login")
def login(req, resp):
resp.session["username"] = "alice"
@api.route("/profile")
def profile(req, resp):
resp.media = {"user": req.session.get("username")}
.. warning::
Always set a secret key in production. The default key is not secret::
api = responder.API(secret_key="your-secret-key-here")
Static Files
------------
Most web applications serve static assets — CSS stylesheets, JavaScript
files, images, fonts. Responder serves these from the ``static/`` directory
by default::
api = responder.API(static_dir="static", static_route="/static")
Place your assets in the ``static/`` directory and they'll be served
automatically at ``/static/style.css``, ``/static/app.js``, etc.
For single-page applications (React, Vue, Angular), you can serve
``index.html`` as the default response for all unmatched routes::
api.add_route("/", static=True)
CORS
----
`CORS <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS>`_ (Cross-
Origin Resource Sharing) is a security mechanism that controls which
websites can make requests to your API. Browsers enforce this — if your
API is at ``api.example.com`` and your frontend is at ``app.example.com``,
the browser will block requests unless your API explicitly allows it.
Enable CORS and configure which origins are allowed::
api = responder.API(cors=True, cors_params={
"allow_origins": ["https://app.example.com"],
"allow_methods": ["GET", "POST"],
"allow_headers": ["*"],
"allow_credentials": True,
"max_age": 600,
})
The default policy is restrictive — you must explicitly allow each origin.
Using ``["*"]`` for allow_origins permits any website to call your API,
which is fine for public APIs but not for private ones.
HSTS
----
`HSTS <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security>`_
(HTTP Strict Transport Security) tells browsers to always use HTTPS when
communicating with your server. Once a browser sees the HSTS header, it
will refuse to connect over plain HTTP, even if the user types ``http://``
in the address bar::
api = responder.API(enable_hsts=True)
Trusted Hosts
-------------
The ``Host`` header in an HTTP request tells the server which domain name
the client used. Attackers can forge this header to trick your application
into generating URLs to malicious domains (a class of attack called *Host
header injection*).
Restrict which hostnames your application accepts::
api = responder.API(allowed_hosts=["example.com", "*.example.com"])
Requests with unrecognized hosts get a ``400 Bad Request``. Wildcard
patterns are supported. By default, all hostnames are allowed.
Request ID
----------
In distributed systems, tracing a single request across multiple services
is essential for debugging. Request IDs are unique identifiers attached to
each request — if something goes wrong, you can search your logs for that
ID and find every related event.
Responder can auto-generate request IDs. If the client sends an
``X-Request-ID`` header (common in microservice architectures), it's
forwarded. Otherwise, a new UUID is generated::
api = responder.API(request_id=True)
The ID appears in the ``X-Request-ID`` response header.
Rate Limiting
-------------
Rate limiting prevents individual clients from overwhelming your API with
too many requests. It's essential for public APIs, and good practice even
for internal ones.
Responder includes a built-in token bucket rate limiter::
from responder.ext.ratelimit import RateLimiter
limiter = RateLimiter(requests=100, period=60) # 100 req/min
limiter.install(api)
When the limit is exceeded, clients receive a ``429 Too Many Requests``
response with a ``Retry-After`` header. Every response includes
``X-RateLimit-Limit`` and ``X-RateLimit-Remaining`` headers so clients
can pace themselves.
The rate limiter is per-client, keyed by IP address.
MessagePack
-----------
`MessagePack <https://msgpack.org/>`_ is a binary serialization format
that's more compact and faster to parse than JSON. It's useful for
high-throughput APIs, IoT devices, and anywhere bandwidth matters.
Responder supports MessagePack alongside JSON and YAML::
# Decode a MessagePack request body
data = await req.media("msgpack")
Content negotiation works too — clients can send
``Accept: application/x-msgpack`` to receive MessagePack responses
instead of JSON.
+191
View File
@@ -0,0 +1,191 @@
Authentication
==============
Every API that handles user data needs authentication — a way to verify
who is making a request. This guide covers the most common patterns:
API keys, JWT tokens, and how to build reusable auth guards with
Responder's before-request hooks.
API Key Authentication
----------------------
The simplest approach. The client sends a secret key in a header, and
your server checks it against a known value. This is common for
server-to-server communication and simple APIs::
API_KEYS = {"sk-abc123", "sk-def456"}
@api.route(before_request=True)
def check_api_key(req, resp):
key = req.headers.get("X-API-Key")
if key not in API_KEYS:
resp.status_code = 401
resp.media = {"error": "Invalid or missing API key"}
Because the before-request hook sets ``resp.status_code``, the route
handler is skipped entirely for unauthorized requests. The client never
reaches your endpoint — the guard catches them first.
The client sends the key like this::
$ curl -H "X-API-Key: sk-abc123" http://localhost:5042/protected
Bearer Token Authentication
----------------------------
Bearer tokens are the standard for modern APIs. The client sends a token
in the ``Authorization`` header, and the server validates it. The most
common format is `JWT <https://jwt.io/>`_ (JSON Web Tokens).
Install PyJWT::
$ uv pip install pyjwt
Create a helper to encode and decode tokens::
import jwt
from datetime import datetime, timedelta
SECRET = "your-secret-key"
def create_token(user_id: int) -> str:
payload = {
"sub": user_id,
"exp": datetime.utcnow() + timedelta(hours=24),
}
return jwt.encode(payload, SECRET, algorithm="HS256")
def verify_token(token: str) -> dict | None:
try:
return jwt.decode(token, SECRET, algorithms=["HS256"])
except jwt.InvalidTokenError:
return None
Add a login endpoint that issues tokens, and a before-request hook that
verifies them::
@api.route("/login", methods=["POST"])
async def login(req, resp):
data = await req.media()
# In a real app, check credentials against a database
if data.get("username") == "admin" and data.get("password") == "secret":
token = create_token(user_id=1)
resp.media = {"token": token}
else:
resp.status_code = 401
resp.media = {"error": "Invalid credentials"}
@api.route(before_request=True)
def auth_guard(req, resp):
# Skip auth for the login endpoint itself
if req.url.path == "/login":
return
auth = req.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
resp.status_code = 401
resp.media = {"error": "Missing bearer token"}
return
token = auth[7:] # Strip "Bearer "
payload = verify_token(token)
if payload is None:
resp.status_code = 401
resp.media = {"error": "Invalid or expired token"}
return
# Store the authenticated user on the request state
req.state.user_id = payload["sub"]
Now any route can access the authenticated user::
@api.route("/me")
def get_me(req, resp):
resp.media = {"user_id": req.state.user_id}
The client flow:
1. ``POST /login`` with credentials → receive a token
2. Include ``Authorization: Bearer <token>`` on every subsequent request
3. The token expires after 24 hours — the client must log in again
Skipping Auth for Public Routes
--------------------------------
The example above skips auth for ``/login`` by checking the path. For
more control, you can use a set of public paths::
PUBLIC_PATHS = {"/login", "/signup", "/health", "/docs", "/schema.yml"}
@api.route(before_request=True)
def auth_guard(req, resp):
if req.url.path in PUBLIC_PATHS:
return
# ... check token
Custom Exception for Auth Errors
---------------------------------
For cleaner code, define a custom exception and register a handler::
class AuthError(Exception):
def __init__(self, message="Unauthorized", status_code=401):
self.message = message
self.status_code = status_code
@api.exception_handler(AuthError)
async def handle_auth_error(req, resp, exc):
resp.status_code = exc.status_code
resp.media = {"error": exc.message}
Now your auth guard can simply raise::
@api.route(before_request=True)
def auth_guard(req, resp):
if req.url.path in PUBLIC_PATHS:
return
if "Authorization" not in req.headers:
raise AuthError("Missing authorization header")
Using Sessions for Web Apps
----------------------------
For traditional web applications (with HTML pages and forms), cookie-based
sessions are simpler than tokens. The browser handles cookies automatically
— no client-side token management needed::
@api.route("/login", methods=["POST"])
async def login(req, resp):
data = await req.media("form")
if data["username"] == "admin" and data["password"] == "secret":
resp.session["user"] = data["username"]
api.redirect(resp, location="/dashboard")
else:
resp.status_code = 401
resp.html = "<p>Invalid credentials</p>"
@api.route("/dashboard")
def dashboard(req, resp):
user = req.session.get("user")
if not user:
api.redirect(resp, location="/login")
return
resp.html = f"<h1>Welcome, {user}!</h1>"
@api.route("/logout")
def logout(req, resp):
resp.session.clear()
api.redirect(resp, location="/login")
Remember to set a proper secret key::
api = responder.API(secret_key="your-production-secret-key")
The session data is signed (not encrypted) — users can read it but
can't tamper with it. Don't store sensitive data like passwords in
sessions.
+192
View File
@@ -0,0 +1,192 @@
Migrating from Flask
====================
If you're coming from Flask, you'll find Responder familiar but different
in a few key ways. This guide maps Flask concepts to their Responder
equivalents and shows you how to translate common patterns.
The Big Differences
-------------------
**No return values.** In Flask, you return a response. In Responder, you
mutate it. This is the single biggest difference:
Flask::
@app.route("/")
def hello():
return "hello, world!"
Responder::
@api.route("/")
def hello(req, resp):
resp.text = "hello, world!"
**Explicit request and response.** Flask uses a global ``request`` object
(via thread-local magic). Responder passes ``req`` and ``resp`` explicitly.
No magic, no import needed — they're right there in the function signature.
**ASGI, not WSGI.** Flask runs on WSGI, which is synchronous. Responder
runs on ASGI, which supports async natively. You can still write sync
views — Responder runs them in a thread pool automatically.
Quick Reference
---------------
.. list-table::
:header-rows: 1
:widths: 40 60
* - Flask
- Responder
* - ``Flask(__name__)``
- ``responder.API()``
* - ``return "text"``
- ``resp.text = "text"``
* - ``return jsonify(data)``
- ``resp.media = data``
* - ``return render_template("t.html", x=1)``
- ``resp.html = api.template("t.html", x=1)``
* - ``request.args["q"]``
- ``req.params["q"]``
* - ``request.json``
- ``await req.media()``
* - ``request.form``
- ``await req.media("form")``
* - ``request.headers["X"]``
- ``req.headers["X"]``
* - ``request.method``
- ``req.method``
* - ``request.cookies["x"]``
- ``req.cookies["x"]``
* - ``session["x"] = 1``
- ``resp.session["x"] = 1``
* - ``abort(404)``
- ``resp.status_code = 404``
* - ``redirect("/new")``
- ``api.redirect(resp, location="/new")``
* - ``@app.before_request``
- ``@api.route(before_request=True)``
* - ``@app.errorhandler(404)``
- ``@api.exception_handler(ValueError)``
* - ``app.run(debug=True)``
- ``api.run(debug=True)``
Route Parameters
----------------
Flask uses ``<angle_brackets>``. Responder uses ``{curly_braces}``
with the same type convertor idea:
Flask::
@app.route("/users/<int:user_id>")
def get_user(user_id):
return jsonify({"id": user_id})
Responder::
@api.route("/users/{user_id:int}")
def get_user(req, resp, *, user_id):
resp.media = {"id": user_id}
Note the ``*`` — route parameters are keyword-only arguments in
Responder. This makes the interface explicit about which arguments
come from the URL.
JSON APIs
---------
Flask::
@app.route("/api/items", methods=["POST"])
def create_item():
data = request.json
# ... create item
return jsonify(item), 201
Responder::
@api.route("/api/items", methods=["POST"])
async def create_item(req, resp):
data = await req.media()
# ... create item
resp.media = item
resp.status_code = 201
The ``await`` is needed because reading the request body is an async
I/O operation. This is more explicit than Flask's approach, and it
means the event loop isn't blocked while waiting for the body to arrive.
Templates
---------
Both use Jinja2. The syntax is nearly identical:
Flask::
@app.route("/hello/<name>")
def hello(name):
return render_template("hello.html", name=name)
Responder::
@api.route("/hello/{name}")
def hello(req, resp, *, name):
resp.html = api.template("hello.html", name=name)
Blueprints → Route Groups
--------------------------
Flask uses Blueprints to organize routes. Responder has route groups:
Flask::
bp = Blueprint("api", __name__, url_prefix="/api")
@bp.route("/users")
def list_users():
return jsonify([])
app.register_blueprint(bp)
Responder::
api_v1 = api.group("/api")
@api_v1.route("/users")
def list_users(req, resp):
resp.media = []
Gradual Migration
-----------------
You don't have to migrate all at once. Responder can mount your existing
Flask app at a subroute, so you can move endpoints over one at a time::
from flask import Flask
flask_app = Flask(__name__)
# Your existing Flask routes stay here
@flask_app.route("/legacy")
def legacy():
return "old endpoint"
# Mount Flask under /old, new routes go on Responder
api.mount("/old", flask_app)
@api.route("/new")
def new_endpoint(req, resp):
resp.media = {"modern": True}
Requests to ``/old/legacy`` go to Flask. Everything else goes to
Responder. When you've moved everything over, remove the mount.
+129
View File
@@ -0,0 +1,129 @@
Writing Middleware
==================
Middleware sits between the server and your route handlers, processing
every request and response that flows through your application. It's the
right tool for cross-cutting concerns — things that apply to *all*
requests, not just specific routes.
Common middleware use cases:
- Request logging and timing
- Authentication and authorization
- Adding security headers
- Request ID generation
- Rate limiting
- Response compression (built-in)
Hooks vs. Middleware
--------------------
Responder gives you two levels of request processing:
**Hooks** (``before_request`` / ``after_request``) run inside Responder's
routing layer. They receive Responder's ``req`` and ``resp`` objects and
are the simplest way to add behavior::
@api.route(before_request=True)
def add_header(req, resp):
resp.headers["X-Powered-By"] = "Responder"
@api.after_request()
def log_request(req, resp):
print(f"{req.method} {req.url.path} -> {resp.status_code}")
**Middleware** runs at the ASGI level, wrapping the entire application.
It's more powerful but more complex — you work with raw ASGI scopes
instead of Responder objects. Use middleware when you need to process
requests *before* they reach Responder's routing, or when you need to
integrate with Starlette middleware.
Using Starlette Middleware
--------------------------
Responder is built on Starlette, so any Starlette middleware works
out of the box::
from starlette.middleware.base import BaseHTTPMiddleware
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
import time
start = time.time()
response = await call_next(request)
duration = time.time() - start
response.headers["X-Response-Time"] = f"{duration:.3f}s"
return response
api.add_middleware(TimingMiddleware)
The ``dispatch`` method receives a Starlette ``Request`` and a
``call_next`` function. Call ``call_next(request)`` to pass the request
to the next middleware (or to your route handler). The return value is
a Starlette ``Response`` that you can modify before it's sent.
Built-in Middleware
-------------------
Responder configures several middleware components automatically:
- **GZipMiddleware** — compresses responses larger than 500 bytes
- **TrustedHostMiddleware** — validates the ``Host`` header
- **ServerErrorMiddleware** — catches unhandled exceptions
- **ExceptionMiddleware** — routes exceptions to your handlers
- **SessionMiddleware** — manages signed cookie sessions
Optional middleware you can enable:
- **CORSMiddleware**``api = responder.API(cors=True)``
- **HTTPSRedirectMiddleware**``api = responder.API(enable_hsts=True)``
Adding Third-Party Middleware
-----------------------------
Any ASGI middleware can be added with ``api.add_middleware()``::
from some_package import SomeMiddleware
api.add_middleware(SomeMiddleware, option1="value", option2=True)
Keyword arguments are passed to the middleware's constructor.
Middleware Order
----------------
Middleware wraps your application like layers of an onion. The *last*
middleware added is the *outermost* layer — it sees the request first
and the response last.
Responder's built-in middleware stack (from outermost to innermost):
1. SessionMiddleware
2. ServerErrorMiddleware
3. CORSMiddleware (if enabled)
4. TrustedHostMiddleware
5. HTTPSRedirectMiddleware (if enabled)
6. GZipMiddleware
7. ExceptionMiddleware
8. Your routes
When you call ``api.add_middleware()``, your middleware is added *outside*
the existing stack. Keep this in mind for ordering dependencies — if
middleware A depends on middleware B having run first, add B before A.
When to Use What
-----------------
- **Simple header additions, logging, auth checks** → use hooks
- **Response transformation, timing, third-party integrations** → use middleware
- **Rate limiting** → use the built-in ``RateLimiter`` (it uses hooks internally)
- **Request ID** → use ``api = responder.API(request_id=True)``
Start with hooks. They're simpler and cover most cases. Graduate to
middleware when hooks aren't enough.
+219
View File
@@ -0,0 +1,219 @@
Building a REST API
===================
This tutorial walks you through building a complete REST API from scratch.
By the end, you'll have a working API with CRUD operations, request
validation, error handling, and interactive documentation.
We'll build a simple book catalog — a service that lets you create, read,
update, and delete books.
Project Setup
-------------
Create a new file called ``app.py``::
import responder
api = responder.API(
title="Book Catalog",
version="1.0",
openapi="3.0.2",
docs_route="/docs",
)
We're enabling OpenAPI documentation from the start. Visit ``/docs`` at
any point to see interactive Swagger UI for your API.
Define Your Models
------------------
We'll use `Pydantic <https://docs.pydantic.dev/>`_ to define our data
models. Pydantic models serve double duty — they validate incoming data
*and* generate OpenAPI schemas automatically::
from pydantic import BaseModel
class BookIn(BaseModel):
"""What the client sends when creating a book."""
title: str
author: str
year: int
isbn: str | None = None
class Book(BaseModel):
"""What the API returns."""
id: int
title: str
author: str
year: int
isbn: str | None = None
``BookIn`` is the *input* model — it doesn't have an ``id`` because the
server assigns that. ``Book`` is the *output* model — it includes
everything. This input/output separation is a common REST API pattern.
In-Memory Storage
-----------------
For this tutorial, we'll store books in a simple dict. In a real
application, you'd use a database (see :doc:`tutorial-sqlalchemy`)::
books_db: dict[int, dict] = {}
next_id = 1
List All Books
--------------
The first endpoint — list all books. This is a ``GET`` request to
``/books``::
@api.route("/books", methods=["GET"], response_model=list)
def list_books(req, resp):
resp.media = list(books_db.values())
In REST API design, ``GET`` requests should never modify data. They're
*safe* and *idempotent* — calling them multiple times has the same effect
as calling them once.
Create a Book
-------------
To create a book, the client sends a ``POST`` request with a JSON body.
We use ``request_model=BookIn`` to validate the input automatically — if
the client sends bad data, they get a ``422`` response with error details::
@api.route("/books", methods=["POST"], check_existing=False,
request_model=BookIn, response_model=Book)
async def create_book(req, resp):
global next_id
data = await req.media()
book = {"id": next_id, **data}
books_db[next_id] = book
next_id += 1
resp.media = book
resp.status_code = 201
Note ``resp.status_code = 201`` — the HTTP ``201 Created`` status code
tells the client that a new resource was successfully created. This is
more informative than a generic ``200 OK``.
Get a Single Book
-----------------
Retrieve a specific book by its ID. The ``{book_id:int}`` route parameter
ensures only integer IDs match — requests like ``/books/abc`` will 404::
@api.route("/books/{book_id:int}", methods=["GET"], response_model=Book)
def get_book(req, resp, *, book_id):
if book_id not in books_db:
resp.status_code = 404
resp.media = {"error": f"Book {book_id} not found"}
return
resp.media = books_db[book_id]
Update a Book
-------------
``PUT`` replaces a resource entirely. The client must send all fields::
@api.route("/books/{book_id:int}", methods=["PUT"], check_existing=False,
request_model=BookIn, response_model=Book)
async def update_book(req, resp, *, book_id):
if book_id not in books_db:
resp.status_code = 404
resp.media = {"error": f"Book {book_id} not found"}
return
data = await req.media()
book = {"id": book_id, **data}
books_db[book_id] = book
resp.media = book
Delete a Book
-------------
``DELETE`` removes a resource. The convention is to return ``204 No Content``
with an empty body on success::
@api.route("/books/{book_id:int}", methods=["DELETE"], check_existing=False)
def delete_book(req, resp, *, book_id):
if book_id not in books_db:
resp.status_code = 404
resp.media = {"error": f"Book {book_id} not found"}
return
del books_db[book_id]
resp.status_code = 204
Error Handling
--------------
Let's add a custom error handler so any ``ValueError`` in our code returns
a clean JSON response instead of a 500 error::
@api.exception_handler(ValueError)
async def handle_value_error(req, resp, exc):
resp.status_code = 400
resp.media = {"error": str(exc)}
Run It
------
Add the standard entry point at the bottom of your file::
if __name__ == "__main__":
api.run()
Start the server::
$ python app.py
Visit ``http://localhost:5042/docs`` to see your interactive API
documentation. You can test every endpoint directly from the browser.
Try It Out
----------
Using ``curl``::
# Create a book
$ curl -X POST http://localhost:5042/books \
-H "Content-Type: application/json" \
-d '{"title": "Dune", "author": "Frank Herbert", "year": 1965}'
# List all books
$ curl http://localhost:5042/books
# Get a specific book
$ curl http://localhost:5042/books/1
# Update a book
$ curl -X PUT http://localhost:5042/books/1 \
-H "Content-Type: application/json" \
-d '{"title": "Dune", "author": "Frank Herbert", "year": 1965, "isbn": "978-0441172719"}'
# Delete a book
$ curl -X DELETE http://localhost:5042/books/1
What's Next
-----------
This tutorial used in-memory storage. For a real application, you'll want
a database. See :doc:`tutorial-sqlalchemy` for how to integrate SQLAlchemy
with Responder using the lifespan pattern.
+254
View File
@@ -0,0 +1,254 @@
Using SQLAlchemy
================
Most real web applications need a database. This guide shows how to
integrate `SQLAlchemy <https://www.sqlalchemy.org/>`_ with Responder,
using async support and the lifespan pattern for connection management.
SQLAlchemy is the most popular Python database toolkit. It gives you an
ORM (Object-Relational Mapper) for working with databases using Python
classes instead of raw SQL, plus a powerful query builder for when you
need fine-grained control.
Installation
------------
Install SQLAlchemy with async support and an async database driver.
We'll use SQLite for simplicity, but the pattern works with PostgreSQL,
MySQL, and any other database SQLAlchemy supports::
$ uv pip install 'sqlalchemy[asyncio]' aiosqlite
Define Your Models
------------------
SQLAlchemy models map Python classes to database tables. Each attribute
becomes a column::
# models.py
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
class Book(Base):
__tablename__ = "books"
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String, nullable=False)
author = Column(String, nullable=False)
year = Column(Integer, nullable=False)
isbn = Column(String, nullable=True)
``DeclarativeBase`` is SQLAlchemy's modern base class (SQLAlchemy 2.0+).
Each model class corresponds to a table, and each ``Column`` corresponds
to a column in that table.
Database Setup
--------------
Create the async engine and session factory. The *engine* manages
the connection pool. The *session* is your unit of work — you use it to
query and modify data within a transaction::
# database.py
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
DATABASE_URL = "sqlite+aiosqlite:///./books.db"
engine = create_async_engine(DATABASE_URL, echo=True)
async_session = async_sessionmaker(engine, expire_on_commit=False)
The ``echo=True`` flag prints all SQL queries to the console — very
helpful during development, but you'll want to disable it in production.
The ``expire_on_commit=False`` flag keeps model attributes accessible
after a commit, which is convenient for returning created objects in
API responses.
Lifespan for Startup and Shutdown
----------------------------------
Use Responder's lifespan context manager to create the database tables
on startup and dispose of connections on shutdown::
# app.py
from contextlib import asynccontextmanager
import responder
from database import engine
from models import Base
@asynccontextmanager
async def lifespan(app):
# Startup: create tables
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
# Shutdown: close all connections
await engine.dispose()
api = responder.API(lifespan=lifespan)
This is the proper way to manage database connections in an async
application. The lifespan context manager ensures that:
1. Tables are created before the first request
2. The connection pool is properly closed when the server shuts down
3. If table creation fails, the server won't start
CRUD Endpoints
--------------
Now let's build the API endpoints. Each one opens a database session,
does its work, and commits or rolls back::
from pydantic import BaseModel
from sqlalchemy import select
from database import async_session
from models import Book
# Pydantic models for request/response validation
class BookIn(BaseModel):
title: str
author: str
year: int
isbn: str | None = None
class BookOut(BaseModel):
id: int
title: str
author: str
year: int
isbn: str | None = None
class Config:
from_attributes = True
The ``from_attributes = True`` config tells Pydantic to read data from
SQLAlchemy model attributes (not just dicts). This lets you pass a
SQLAlchemy ``Book`` object directly to ``BookOut``.
**List all books**::
@api.route("/books", methods=["GET"])
async def list_books(req, resp):
async with async_session() as session:
result = await session.execute(select(Book))
books = result.scalars().all()
resp.media = [BookOut.model_validate(b).model_dump() for b in books]
**Create a book**::
@api.route("/books", methods=["POST"], check_existing=False,
request_model=BookIn, response_model=BookOut)
async def create_book(req, resp):
data = await req.media()
async with async_session() as session:
book = Book(**data)
session.add(book)
await session.commit()
await session.refresh(book)
resp.media = BookOut.model_validate(book).model_dump()
resp.status_code = 201
**Get a single book**::
@api.route("/books/{book_id:int}", methods=["GET"])
async def get_book(req, resp, *, book_id):
async with async_session() as session:
book = await session.get(Book, book_id)
if book is None:
resp.status_code = 404
resp.media = {"error": "Book not found"}
return
resp.media = BookOut.model_validate(book).model_dump()
**Update a book**::
@api.route("/books/{book_id:int}", methods=["PUT"], check_existing=False,
request_model=BookIn)
async def update_book(req, resp, *, book_id):
data = await req.media()
async with async_session() as session:
book = await session.get(Book, book_id)
if book is None:
resp.status_code = 404
resp.media = {"error": "Book not found"}
return
for key, value in data.items():
setattr(book, key, value)
await session.commit()
await session.refresh(book)
resp.media = BookOut.model_validate(book).model_dump()
**Delete a book**::
@api.route("/books/{book_id:int}", methods=["DELETE"], check_existing=False)
async def delete_book(req, resp, *, book_id):
async with async_session() as session:
book = await session.get(Book, book_id)
if book is None:
resp.status_code = 404
resp.media = {"error": "Book not found"}
return
await session.delete(book)
await session.commit()
resp.status_code = 204
Run It
------
::
if __name__ == "__main__":
api.run()
Start the server and you'll see SQLAlchemy's SQL echo in the console.
The SQLite database file ``books.db`` is created automatically on first
startup.
Using PostgreSQL
----------------
To switch to PostgreSQL, just change the connection URL and driver::
$ uv pip install asyncpg
::
DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/mydb"
Everything else stays the same. SQLAlchemy abstracts the database
differences so your application code doesn't need to change.
Tips
----
- Use ``async with async_session() as session`` for every request.
Don't share sessions across requests — each request should get its
own session and transaction.
- For complex queries, use SQLAlchemy's ``select()`` with ``.where()``,
``.order_by()``, ``.limit()``, and ``.offset()`` — it composes
naturally.
- In production, use connection pooling (SQLAlchemy does this by
default) and set pool size limits appropriate for your database.
- Consider `Alembic <https://alembic.sqlalchemy.org/>`_ for database
migrations — it tracks schema changes over time so you can evolve
your database without losing data.
+171
View File
@@ -0,0 +1,171 @@
WebSocket Tutorial
==================
HTTP is request-response — the client asks, the server answers, and the
connection closes. WebSockets upgrade that into a persistent, bidirectional
channel where both sides can send messages at any time. This is what powers
chat apps, live dashboards, multiplayer games, and collaborative editors.
This tutorial builds a simple chat room to show how WebSockets work in
Responder.
How WebSockets Work
-------------------
1. The client sends a normal HTTP request with an ``Upgrade: websocket``
header.
2. The server accepts the upgrade and the connection switches protocols.
3. Both sides can now send messages freely — no more request/response.
4. Either side can close the connection at any time.
In Responder, WebSocket routes receive a ``ws`` object instead of
``req`` and ``resp``. The ``ws`` object has methods for accepting the
connection, sending and receiving data, and closing.
Echo Server
-----------
The simplest WebSocket — echoes everything back::
@api.route("/ws", websocket=True)
async def echo(ws):
await ws.accept()
while True:
data = await ws.receive_text()
await ws.send_text(f"Echo: {data}")
The ``await ws.accept()`` call completes the WebSocket handshake. After
that, you're in a loop — receive a message, send a response.
Test it with a WebSocket client::
$ pip install websocket-client
$ python -c "
import websocket
ws = websocket.create_connection('ws://localhost:5042/ws')
ws.send('hello')
print(ws.recv()) # Echo: hello
ws.close()
"
Chat Room
---------
A chat room needs to broadcast messages to all connected clients. We keep
a set of active connections and iterate through them when someone sends
a message::
connected = set()
@api.route("/chat", websocket=True)
async def chat(ws):
await ws.accept()
connected.add(ws)
try:
while True:
message = await ws.receive_text()
# Broadcast to all connected clients
for client in connected:
await client.send_text(message)
except Exception:
pass
finally:
connected.discard(ws)
The ``try/finally`` block ensures we remove disconnected clients from
the set, even if the connection drops unexpectedly.
Data Formats
------------
WebSockets support three data formats:
**Text** — plain strings::
await ws.send_text("hello")
message = await ws.receive_text()
**JSON** — auto-serialized Python objects::
await ws.send_json({"type": "update", "data": [1, 2, 3]})
message = await ws.receive_json()
**Binary** — raw bytes, useful for images, audio, or custom protocols::
await ws.send_bytes(b"\x00\x01\x02")
data = await ws.receive_bytes()
HTML Client
-----------
Here's a minimal HTML page that connects to the chat room. The browser's
built-in ``WebSocket`` API handles everything — no libraries needed:
.. code-block:: html
<!DOCTYPE html>
<html>
<body>
<div id="messages"></div>
<input id="input" placeholder="Type a message..." />
<script>
const ws = new WebSocket("ws://localhost:5042/chat");
const messages = document.getElementById("messages");
const input = document.getElementById("input");
ws.onmessage = (event) => {
const p = document.createElement("p");
p.textContent = event.data;
messages.appendChild(p);
};
input.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
ws.send(input.value);
input.value = "";
}
});
</script>
</body>
</html>
Save this as ``static/index.html`` and serve it with Responder's
built-in static file support.
Before-Request Hooks for WebSockets
------------------------------------
You can run code before a WebSocket connection is established, just like
HTTP before-request hooks. This is useful for authentication::
@api.before_request(websocket=True)
async def ws_auth(ws):
# Check for a token in the query string
# (WebSocket headers are limited in browsers)
await ws.accept()
WebSocket before-request hooks receive the ``ws`` object and must call
``await ws.accept()`` if they want the connection to proceed.
Testing WebSockets
------------------
Use Starlette's ``TestClient`` for WebSocket tests::
from starlette.testclient import TestClient
def test_echo():
client = TestClient(api)
with client.websocket_connect("/ws") as ws:
ws.send_text("hello")
assert ws.receive_text() == "Echo: hello"
The ``websocket_connect`` context manager handles the connection
lifecycle — it connects on enter and disconnects on exit.
+19
View File
@@ -0,0 +1,19 @@
# Example HTTP service definition, using Responder.
# https://pypi.org/project/responder/
import responder
api = responder.API()
@api.route("/")
async def index(req, resp):
resp.text = "hello, world!"
@api.route("/{greeting}")
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
if __name__ == "__main__":
api.run()
+26
View File
@@ -0,0 +1,26 @@
# Example showing the lifespan context manager pattern.
# https://pypi.org/project/responder/
from contextlib import asynccontextmanager
import responder
@asynccontextmanager
async def lifespan(app):
# Startup: initialize resources
print("Starting up...")
yield
# Shutdown: clean up resources
print("Shutting down...")
api = responder.API(lifespan=lifespan)
@api.route("/{greeting}")
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
if __name__ == "__main__":
api.run()
+31
View File
@@ -0,0 +1,31 @@
"""Mount marimo notebooks inside a Responder API.
Requirements:
pip install responder marimo
Usage:
python examples/marimo_mount.py
Then visit:
http://127.0.0.1:5042/hello → Responder JSON endpoint
http://127.0.0.1:5042/notebooks/ → Interactive marimo notebook
"""
import marimo
import responder
api = responder.API()
@api.route("/hello")
def hello(req, resp):
resp.media = {"message": "Hello from Responder!"}
# Mount marimo notebooks at /notebooks
server = marimo.create_asgi_app().with_app(path="", root="notebooks/hello.py")
api.mount("/notebooks", server.build())
if __name__ == "__main__":
api.run()
+76
View File
@@ -0,0 +1,76 @@
# Complete REST API example with Pydantic validation.
# https://responder.kennethreitz.org/tutorial-rest.html
from pydantic import BaseModel
import responder
class BookIn(BaseModel):
title: str
author: str
year: int
isbn: str | None = None
class BookOut(BaseModel):
id: int
title: str
author: str
year: int
isbn: str | None = None
api = responder.API(
title="Book Catalog",
version="1.0",
openapi="3.0.2",
docs_route="/docs",
)
books_db: dict[int, dict] = {}
next_id = 1
@api.route("/books", methods=["GET"])
def list_books(req, resp):
resp.media = list(books_db.values())
@api.route(
"/books",
methods=["POST"],
check_existing=False,
request_model=BookIn,
response_model=BookOut,
)
async def create_book(req, resp):
global next_id
data = await req.media()
book = {"id": next_id, **data}
books_db[next_id] = book
next_id += 1
resp.media = book
resp.status_code = 201
@api.route("/books/{book_id:int}", methods=["GET"])
def get_book(req, resp, *, book_id):
if book_id not in books_db:
resp.status_code = 404
resp.media = {"error": f"Book {book_id} not found"}
return
resp.media = books_db[book_id]
@api.route("/books/{book_id:int}", methods=["DELETE"], check_existing=False)
def delete_book(req, resp, *, book_id):
if book_id not in books_db:
resp.status_code = 404
resp.media = {"error": f"Book {book_id} not found"}
return
del books_db[book_id]
resp.status_code = 204
if __name__ == "__main__":
api.run()
+42
View File
@@ -0,0 +1,42 @@
# Server-Sent Events streaming example.
# https://responder.kennethreitz.org/tour.html#server-sent-events-sse
import asyncio
import responder
api = responder.API()
@api.route("/")
def index(req, resp):
resp.html = """
<!DOCTYPE html>
<html>
<body>
<h1>SSE Stream</h1>
<div id="events"></div>
<script>
const source = new EventSource("/stream");
const events = document.getElementById("events");
source.onmessage = (e) => {
const p = document.createElement("p");
p.textContent = e.data;
events.appendChild(p);
};
</script>
</body>
</html>
"""
@api.route("/stream")
async def stream(req, resp):
@resp.sse
async def events():
for i in range(20):
yield {"data": f"Event #{i}"}
await asyncio.sleep(0.5)
if __name__ == "__main__":
api.run()
+25
View File
@@ -0,0 +1,25 @@
# Example HTTP service definition, using Responder.
# https://pypi.org/project/responder/
import responder
api = responder.API()
@api.route("/")
async def index(req, resp):
resp.text = "Welcome"
@api.route("/user")
async def user_create(req, resp):
data = await req.media()
resp.text = f"Hello, {data['username']}"
@api.route("/user/{identifier}")
async def user_get(req, resp, *, identifier):
resp.text = f"Hello, user {identifier}"
if __name__ == "__main__":
api.run()
+59
View File
@@ -0,0 +1,59 @@
# WebSocket chat room example.
# https://responder.kennethreitz.org/tutorial-websockets.html
from starlette.websockets import WebSocketDisconnect
import responder
api = responder.API()
connected = set()
@api.route("/")
def index(req, resp):
resp.html = """
<!DOCTYPE html>
<html>
<body>
<h1>Chat Room</h1>
<div id="messages" style="height:300px;overflow-y:scroll;border:1px solid #ccc;padding:10px;"></div>
<input id="input" placeholder="Type a message..." style="width:300px;" />
<script>
const ws = new WebSocket(`ws://${location.host}/chat`);
const messages = document.getElementById("messages");
const input = document.getElementById("input");
ws.onmessage = (e) => {
const p = document.createElement("p");
p.textContent = e.data;
messages.appendChild(p);
messages.scrollTop = messages.scrollHeight;
};
input.addEventListener("keypress", (e) => {
if (e.key === "Enter" && input.value) {
ws.send(input.value);
input.value = "";
}
});
</script>
</body>
</html>
""" # noqa: E501
@api.route("/chat", websocket=True)
async def chat(ws):
await ws.accept()
connected.add(ws)
try:
while True:
message = await ws.receive_text()
for client in connected:
await client.send_text(message)
except WebSocketDisconnect:
pass
finally:
connected.discard(ws)
if __name__ == "__main__":
api.run()
Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve"><polygon points="32.625,51 21.836,51 28.536,13 39.325,13 "></polygon><polygon points="49.107,51 38.319,51 45.019,13 55.808,13 "></polygon><rect x="9" y="18" width="12" height="12"></rect><rect x="9" y="33" width="12" height="12"></rect></svg>

After

Width:  |  Height:  |  Size: 430 B

+49431
View File
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

-13
View File
@@ -1,13 +0,0 @@
import graphene
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return "Hello " + name
schema = graphene.Schema(query=Query)
result = schema.execute("{ hello }")
print(result.data["hello"])
+167
View File
@@ -0,0 +1,167 @@
[build-system]
build-backend = "setuptools.build_meta"
requires = [
"setuptools>=42",
]
[project]
name = "responder"
description = "A familiar HTTP Service Framework for Python."
readme = "README.md"
license = { text = "Apache 2.0" }
authors = [
{ name = "Kenneth Reitz", email = "me@kennethreitz.org" },
]
requires-python = ">=3.10"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: Free Threading",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Internet :: WWW/HTTP",
]
dynamic = [ "version" ]
dependencies = [
"a2wsgi",
"apispec>=1",
"chardet",
"docopt-ng",
"graphene>=3",
"graphql-core>=3.1",
"marshmallow",
"msgpack",
"pueblo[sfa-full]>=0.0.11",
"pydantic>=2",
"python-multipart",
"starlette[full]>=1",
"uvicorn[standard]",
]
optional-dependencies.develop = [
"pyproject-fmt",
"ruff",
"validate-pyproject",
]
optional-dependencies.docs = [
"alabaster<1.1",
"myst-parser",
"sphinx>=5,<9",
"sphinx-autobuild",
"sphinx-copybutton",
"sphinx-design-elements",
]
optional-dependencies.release = [ "build", "twine" ]
optional-dependencies.test = [
"flask",
"mypy",
"pytest",
"pytest-cov",
"pytest-mock",
"pytest-rerunfailures",
]
urls.Documentation = "https://responder.kennethreitz.org"
urls.Homepage = "https://github.com/kennethreitz/responder"
urls.Issues = "https://github.com/kennethreitz/responder/issues"
urls.Repository = "https://github.com/kennethreitz/responder"
scripts.responder = "responder.ext.cli:cli"
[tool.setuptools]
dynamic.version = { attr = "responder.__version__.__version__" }
package-data.responder = [ "py.typed", "ext/openapi/docs/*.html" ]
packages.find.exclude = [ "tests" ]
[tool.ruff]
line-length = 90
extend-exclude = [
"docs/source/conf.py",
]
lint.select = [
# Builtins
"A",
# Bugbear
"B",
# comprehensions
"C4",
# Pycodestyle
"E",
# eradicate
"ERA",
# Pyflakes
"F",
# isort
"I",
# pandas-vet
"PD",
# return
"RET",
# Bandit
"S",
# print
"T20",
"W",
# flake8-2020
"YTT",
]
lint.extend-ignore = [
"S101", # Allow use of `assert`.
]
lint.per-file-ignores."responder/util/cmd.py" = [ "A005" ] # Module shadows a Python standard-library module
lint.per-file-ignores."tests/*" = [
"ERA001", # Found commented-out code.
"S101", # Allow use of `assert`, and `print`.
]
[tool.mypy]
packages = [
"responder",
]
exclude = []
check_untyped_defs = true
explicit_package_bases = true
ignore_missing_imports = true
implicit_optional = true
install_types = true
namespace_packages = true
non_interactive = true
[tool.pytest]
ini_options.addopts = """
-rfEXs -p pytester --strict-markers --verbosity=3
--cov --cov-report=term-missing --cov-report=xml
"""
ini_options.filterwarnings = [
"error::UserWarning",
]
ini_options.log_level = "DEBUG"
ini_options.log_cli_level = "DEBUG"
ini_options.log_format = "%(asctime)-15s [%(name)-36s] %(levelname)-8s: %(message)s"
ini_options.minversion = "2.0"
ini_options.testpaths = [
"responder",
"tests",
]
ini_options.markers = []
ini_options.xfail_strict = true
[tool.coverage]
run.branch = false
run.omit = [
"*.html",
"tests/*",
]
report.exclude_lines = [
"# pragma: no cover",
"raise NotImplemented",
]
report.fail_under = 0
report.show_missing = true
+18 -1
View File
@@ -1 +1,18 @@
from .core import *
"""
Responder - a familiar HTTP Service Framework.
This module exports the core functionality of the Responder framework,
including the API, Request, and Response classes.
"""
from . import ext
from .__version__ import __version__
from .core import API, Request, Response
__all__ = [
"API",
"Request",
"Response",
"__version__",
"ext",
]
+1
View File
@@ -0,0 +1 @@
__version__ = "3.5.0"
+559 -122
View File
@@ -1,166 +1,603 @@
import asyncio
import inspect
import os
from pathlib import Path
import graphene
__all__ = ["API"]
from whitenoise import WhiteNoise
from wsgiadapter import WSGIAdapter as RequestsWSGIAdapter
from requests import Session as RequestsSession
import uvicorn
from starlette.middleware.cors import CORSMiddleware
from starlette.middleware.errors import ServerErrorMiddleware
from starlette.middleware.exceptions import ExceptionMiddleware
from starlette.middleware.gzip import GZipMiddleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
from . import models
from .status import HTTP_404
from . import status_codes
from .background import BackgroundQueue
from .formats import get_formats
from .models import Request, Response
from .routes import Router
from .staticfiles import StaticFiles
from .statics import DEFAULT_CORS_PARAMS, DEFAULT_OPENAPI_THEME, DEFAULT_SECRET_KEY
from .templates import Templates
class BaseAPI:
__slots__ = ["routes"]
class API:
"""The primary web-service class.
def __init__(self):
self.routes = {}
:param static_dir: The directory to use for static files. Will be created for you if it doesn't already exist.
:param templates_dir: The directory to use for templates. Will be created for you if it doesn't already exist.
:param auto_escape: If ``True``, HTML and XML templates will automatically be escaped.
:param enable_hsts: If ``True``, send all responses to HTTPS URLs.
:param openapi_theme: OpenAPI documentation theme, must be one of ``elements``, ``rapidoc``, ``redoc``, ``swagger_ui``
""" # noqa: E501
def _wsgi_app(self, environ, start_response):
# def wsgi_app(self, request):
"""The actual WSGI application. This is not implemented in
:meth:`__call__` so that middlewares can be applied without
losing a reference to the app object. Instead of doing this::
status_codes = status_codes
app = MyMiddleware(app)
def __init__(
self,
*,
debug=False,
title=None,
version=None,
description=None,
terms_of_service=None,
contact=None,
license=None, # noqa: A002
openapi=None,
openapi_route="/schema.yml",
static_dir="static",
static_route="/static",
templates_dir="templates",
auto_escape=True,
secret_key=DEFAULT_SECRET_KEY,
enable_hsts=False,
docs_route=None,
cors=False,
cors_params=DEFAULT_CORS_PARAMS,
allowed_hosts=None,
openapi_theme=DEFAULT_OPENAPI_THEME,
lifespan=None,
request_id=False,
):
"""Create a new Responder API instance.
It's a better idea to do this instead::
:param debug: If ``True``, enable debug mode with verbose error pages.
:param title: The title of the API, used in OpenAPI documentation.
:param version: The version string for the API (e.g. ``"1.0"``).
:param description: A longer description of the API for OpenAPI docs.
:param terms_of_service: URL to the API's terms of service.
:param contact: Contact information dict for the API (``name``, ``url``, ``email``).
:param license: License information dict (``name``, ``url``).
:param openapi: The OpenAPI version string (e.g. ``"3.0.2"``). Enables OpenAPI schema generation.
:param openapi_route: The URL path for the OpenAPI schema (default ``"/schema.yml"``).
:param static_dir: Directory for static files. Set to ``None`` to disable. Created automatically if missing.
:param static_route: URL prefix for serving static files (default ``"/static"``).
:param templates_dir: Directory for Jinja2 templates (default ``"templates"``).
:param auto_escape: If ``True``, auto-escape HTML/XML in templates.
:param secret_key: Secret key for signing cookie-based sessions. **Always set this in production.**
:param enable_hsts: If ``True``, redirect all HTTP requests to HTTPS.
:param docs_route: URL path for interactive API docs (e.g. ``"/docs"``). Enables OpenAPI if not already set.
:param cors: If ``True``, enable CORS middleware.
:param cors_params: Dict of CORS configuration (``allow_origins``, ``allow_methods``, etc.).
:param allowed_hosts: List of allowed hostnames (e.g. ``["example.com"]``). Defaults to ``["*"]``.
:param openapi_theme: Documentation UI theme: ``"swagger_ui"``, ``"redoc"``, ``"rapidoc"``, or ``"elements"``.
:param lifespan: An async context manager for startup/shutdown logic.
:param request_id: If ``True``, add ``X-Request-ID`` headers to all responses.
""" # noqa: E501
self.background = BackgroundQueue()
app.wsgi_app = MyMiddleware(app.wsgi_app)
self.secret_key = secret_key
Then you still have the original application object around and
can continue to call methods on it.
self.router = Router(lifespan=lifespan)
.. versionchanged:: 0.7
Teardown events for the request and app contexts are called
even if an unhandled error occurs. Other events may not be
called depending on when an error occurs during dispatch.
See :ref:`callbacks-and-errors`.
if static_dir is not None:
if static_route is None:
static_route = ""
static_dir = Path(static_dir).resolve()
:param environ: A WSGI environment.
:param start_response: A callable accepting a status code,
a list of headers, and an optional exception context to
start the response.
"""
self.static_dir = static_dir
self.static_route = static_route
req = models.Request.from_environ(environ)
resp = self._dispatch_request(req)
self.hsts_enabled = enable_hsts
self.cors = cors
self.cors_params = cors_params
self.debug = debug
return resp(environ, start_response)
if not allowed_hosts:
allowed_hosts = ["*"]
self.allowed_hosts = allowed_hosts
def wsgi_app(self, environ, start_response):
return self.whitenoise(environ, start_response)
if self.static_dir is not None:
self.static_dir.mkdir(parents=True, exist_ok=True)
self.mount(self.static_route, self.static_app)
def __call__(self, environ, start_response):
"""The WSGI server calls the Flask application object as the
WSGI application. This calls :meth:`wsgi_app` which can be
wrapped to applying middleware."""
return self.wsgi_app(environ, start_response)
self.formats = get_formats()
def path_matches_route(self, url):
for (route, view) in self.routes.items():
if url == route:
return route
self._session = None
def _dispatch_request(self, req):
route = self.path_matches_route(req.path)
resp = models.Response(req=req)
self.default_endpoint = None
self.app = ExceptionMiddleware(self.router, debug=debug)
self.add_middleware(GZipMiddleware)
if route:
if self.hsts_enabled:
self.add_middleware(HTTPSRedirectMiddleware)
self.add_middleware(TrustedHostMiddleware, allowed_hosts=self.allowed_hosts)
if self.cors:
self.add_middleware(CORSMiddleware, **self.cors_params)
self.add_middleware(ServerErrorMiddleware, debug=debug)
self.add_middleware(SessionMiddleware, secret_key=self.secret_key)
if openapi or docs_route:
try:
self.routes[route](req, resp)
# The request is using class-based views.
except TypeError:
try:
view = self.routes[route]()
# GraphQL Schema.
except TypeError:
view = self.routes[route]
self.graphql_response(req, resp, schema=view)
from .ext.openapi import OpenAPISchema
except ImportError as ex:
raise ImportError(
"The dependencies for the OpenAPI extension are not installed. "
"Install them using: pip install responder"
) from ex
# Run on_request first.
try:
getattr(view, "on_request")(req, resp)
except AttributeError:
pass
self.openapi = OpenAPISchema(
app=self,
title=title,
version=version,
openapi=openapi,
docs_route=docs_route,
description=description,
terms_of_service=terms_of_service,
contact=contact,
license=license,
openapi_route=openapi_route,
static_route=static_route,
openapi_theme=openapi_theme,
)
# Then on_get.
method = req.method.lower()
self.templates = Templates(directory=templates_dir)
try:
getattr(view, f"on_{method}")(req, resp)
except AttributeError:
pass
else:
self.default_response(req, resp)
if request_id:
import uuid as _uuid
return resp
def _add_request_id(req, resp):
rid = req.headers.get("X-Request-ID", str(_uuid.uuid4()))
resp.headers["X-Request-ID"] = rid
self.router.after_request(_add_request_id)
@property
def static_dir(self):
return Path(".")
def requests(self):
"""A test client connected to the ASGI app. Lazily initialized."""
return self.session()
@property
def static_app(self):
"""The Starlette ``StaticFiles`` application for serving static assets."""
if not hasattr(self, "_static_app"):
assert self.static_dir is not None
self._static_app = StaticFiles(directory=self.static_dir)
return self._static_app
class API(BaseAPI):
__slots__ = ("routes", "_session", "whitenoise", "static_dir")
def before_request(self, websocket=False):
"""Register a function to run before every request.
def __init__(self, static="static"):
super().__init__()
self._session = None
self.static_dir = Path(os.path.abspath(static))
If the hook sets ``resp.status_code``, the route handler is skipped
and the response is sent immediately (short-circuiting).
# Make the static directory if it doesn't exist.
os.makedirs(self.static_dir, exist_ok=True)
:param websocket: If ``True``, register as a WebSocket before-request hook instead of HTTP.
# Mount the whitenoise application.
self.whitenoise = WhiteNoise(self._wsgi_app, root=str(self.static_dir))
Usage::
def add_route(self, route, view, *, check_existing=True, graphiql=False):
if check_existing:
assert route not in self.routes
@api.before_request()
def check_auth(req, resp):
if "Authorization" not in req.headers:
resp.status_code = 401
resp.media = {"error": "unauthorized"}
# TODO: Support grpahiql.
""" # noqa: E501
self.routes[route] = view
def default_response(self, req, resp):
resp.status_code = HTTP_404
resp.text = "Not found."
@staticmethod
def _resolve_graphql_query(req):
# Support query/q in form data.
if "query" in req.data:
return req.data["query"]
if "q" in req.data:
return req.data["q"]
# Support query/q in params.
if "query" in req.params:
return req.params["query"][0]
if "q" in req.params:
return req.parama["q"][0]
# Otherwise, the request text is used (typical).
# TODO: Make some assertions about content-type here.
return req.text
def graphql_response(self, req, resp, schema):
query = self._resolve_graphql_query(req)
result = schema.execute(query)
resp.media = dict(result.data)
def route(self, route, **options):
def decorator(f):
self.add_route(route, f)
self.router.before_request(f, websocket=websocket)
return f
return decorator
def session(self, base_url="http://app"):
def after_request(self):
"""Register a function to run after every request.
Usage::
@api.after_request()
def add_request_id(req, resp):
resp.headers["X-Request-ID"] = str(uuid.uuid4())
"""
def decorator(f):
self.router.after_request(f)
return f
return decorator
def add_middleware(self, middleware_cls, **middleware_config):
"""Add ASGI middleware to the application.
Middleware wraps the entire application and can inspect or modify
every request and response. Middleware is applied in reverse order —
the last middleware added runs first.
:param middleware_cls: A Starlette-compatible middleware class.
:param middleware_config: Keyword arguments passed to the middleware constructor.
Usage::
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
api.add_middleware(HTTPSRedirectMiddleware)
"""
self.app = middleware_cls(self.app, **middleware_config)
def exception_handler(self, exception_cls):
"""Register a handler for a specific exception type.
Usage::
@api.exception_handler(ValueError)
async def handle_value_error(req, resp, exc):
resp.status_code = 400
resp.media = {"error": str(exc)}
"""
def decorator(func):
async def _handler(request, exc):
from starlette.responses import Response as StarletteResp
req = Request(request.scope, request.receive, formats=get_formats())
resp = Response(req=req, formats=get_formats())
if inspect.iscoroutinefunction(func):
await func(req, resp, exc)
else:
func(req, resp, exc)
if resp.status_code is None:
resp.status_code = 500
body, headers = await resp.body
return StarletteResp(
content=body, status_code=resp.status_code, headers=headers
)
# Register with the ExceptionMiddleware
self.router._exception_handlers = getattr(
self.router, "_exception_handlers", {}
)
self.router._exception_handlers[exception_cls] = _handler
# Also register on the ASGI app chain
from starlette.middleware.exceptions import ExceptionMiddleware as EM
app = self.app
while app is not None:
if isinstance(app, EM):
app.add_exception_handler(exception_cls, _handler)
break
app = getattr(app, "app", None)
return func
return decorator
def schema(self, name, **options):
"""
Decorator for creating new routes around function and class definitions.
Usage::
from marshmallow import Schema, fields
@api.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
"""
def decorator(f):
self.openapi.add_schema(name=name, schema=f, **options)
return f
return decorator
def path_matches_route(self, path):
"""Given a path portion of a URL, tests that it matches against any registered route.
:param path: The path portion of a URL, to test all known routes against.
""" # noqa: E501 (Line too long)
for route in self.router.routes:
match, _ = route.matches(path)
if match:
return route
return None
def add_route(
self,
route=None,
endpoint=None,
*,
default=False,
static=True,
check_existing=True,
websocket=False,
before_request=False,
methods=None,
):
"""Adds a route to the API.
:param route: A string representation of the route.
:param endpoint: The endpoint for the route -- can be a callable, or a class.
:param default: If ``True``, all unknown requests will route to this view.
:param static: If ``True``, and no endpoint was passed, render "static/index.html".
Also, it will become a default route.
:param methods: Optional list of HTTP methods (e.g. ``["GET", "POST"]``).
""" # noqa: E501
if static:
assert self.static_dir is not None
if not endpoint:
endpoint = self._static_response
default = True
self.router.add_route(
route,
endpoint,
default=default,
websocket=websocket,
before_request=before_request,
check_existing=check_existing,
methods=methods,
)
async def _static_response(self, req, resp):
assert self.static_dir is not None
index = (self.static_dir / "index.html").resolve()
if index.exists():
resp.html = index.read_text()
else:
resp.status_code = status_codes.HTTP_404 # type: ignore[attr-defined]
resp.text = "Not found."
def redirect(
self,
resp,
location,
*,
set_text=True,
status_code=status_codes.HTTP_301, # type: ignore[attr-defined]
):
"""
Redirects a given response to a given location.
:param resp: The Response to mutate.
:param location: The location of the redirect.
:param set_text: If ``True``, sets the Redirect body content automatically.
:param status_code: an `API.status_codes` attribute, or an integer,
representing the HTTP status code of the redirect.
"""
resp.redirect(location, set_text=set_text, status_code=status_code)
def on_event(self, event_type: str, **args):
"""Decorator for registering functions or coroutines to run at certain events
Supported events: startup, shutdown
Usage::
@api.on_event('startup')
async def open_database_connection_pool():
...
@api.on_event('shutdown')
async def close_database_connection_pool():
...
"""
def decorator(func):
self.add_event_handler(event_type, func, **args)
return func
return decorator
def add_event_handler(self, event_type, handler):
"""Adds an event handler to the API.
:param event_type: A string in ("startup", "shutdown")
:param handler: The function to run. Can be either a function or a coroutine.
"""
self.router.add_event_handler(event_type, handler)
def route(self, route=None, *, request_model=None, response_model=None, **options):
"""Decorator for creating new routes around function and class definitions.
Usage::
@api.route("/hello")
def hello(req, resp):
resp.text = "hello, world!"
With Pydantic models for OpenAPI documentation::
from pydantic import BaseModel
class ItemIn(BaseModel):
name: str
price: float
class ItemOut(BaseModel):
id: int
name: str
price: float
@api.route("/items", methods=["POST"],
request_model=ItemIn, response_model=ItemOut)
async def create_item(req, resp):
data = await req.media()
resp.media = {"id": 1, **data}
"""
def decorator(f):
if request_model is not None:
f._request_model = request_model
if hasattr(self, "openapi"):
self.openapi.add_schema(
request_model.__name__, request_model, check_existing=False
)
if response_model is not None:
f._response_model = response_model
if hasattr(self, "openapi"):
self.openapi.add_schema(
response_model.__name__, response_model, check_existing=False
)
self.add_route(route, f, **options)
return f
return decorator
def graphql(self, route="/graphql", *, schema):
"""Mount a GraphQL API at the given route.
Usage::
import graphene
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return f"Hello {name}"
api.graphql("/graphql", schema=graphene.Schema(query=Query))
:param route: The URL path for the GraphQL endpoint.
:param schema: A Graphene schema instance.
"""
from .ext.graphql import GraphQLView
self.add_route(route, GraphQLView(api=self, schema=schema))
def mount(self, route, app):
"""Mounts an WSGI / ASGI application at a given route.
:param route: String representation of the route to be used
(shouldn't be parameterized).
:param app: The other WSGI / ASGI app.
"""
self.router.apps.update({route: app})
def session(self, base_url="http://;"):
"""Testing HTTP client. Returns a Starlette TestClient instance,
able to send HTTP requests to the Responder application.
:param base_url: The base URL for the test client.
"""
if self._session is None:
session = RequestsSession()
session.mount(base_url, RequestsWSGIAdapter(self))
self._session = session
from starlette.testclient import TestClient
self._session = TestClient(self, base_url=base_url)
return self._session
def url_for(self, endpoint, **params):
"""Given an endpoint, returns a rendered URL for its route.
:param endpoint: The route endpoint you're searching for.
:param params: Data to pass into the URL generator (for parameterized URLs).
"""
return self.router.url_for(endpoint, **params)
def template(self, filename, *args, **kwargs):
r"""Render a Jinja2 template file with the provided values.
:param filename: The filename of the jinja2 template, in ``templates_dir``.
:param \*args: Data to pass into the template.
:param \*\*kwargs: Data to pass into the template.
"""
return self.templates.render(filename, *args, **kwargs)
def template_string(self, source, *args, **kwargs):
r"""Render a Jinja2 template string with the provided values.
:param source: The template to use, a Jinja2 template string.
:param \*args: Data to pass into the template.
:param \*\*kwargs: Data to pass into the template.
"""
return self.templates.render_string(source, *args, **kwargs)
def serve(self, *, address=None, port=None, debug=False, **options):
"""
Run the application with uvicorn.
If the ``PORT`` environment variable is set, requests will be served on that port
automatically to all known hosts.
:param address: The address to bind to.
:param port: The port to bind to. If none is provided, one will be selected at random.
:param debug: Whether to run application in debug mode.
:param options: Additional keyword arguments to send to ``uvicorn.run()``.
""" # noqa: E501
if "PORT" in os.environ:
if address is None:
address = "0.0.0.0" # noqa: S104
port = int(os.environ["PORT"])
if address is None:
address = "127.0.0.1"
if port is None:
port = 5042
if debug:
options["log_level"] = "debug"
uvicorn.run(self, host=address, port=port, **options)
def run(self, **kwargs):
"""Run the application. Shorthand for :meth:`serve` that inherits the ``debug`` setting.
:param kwargs: Keyword arguments passed through to :meth:`serve`.
""" # noqa: E501
if "debug" not in kwargs:
kwargs.update({"debug": self.debug})
self.serve(**kwargs)
def group(self, prefix):
"""Create a route group with a shared URL prefix.
Usage::
v1 = api.group("/v1")
@v1.route("/users")
def list_users(req, resp):
resp.media = []
@v1.route("/users/{id:int}")
def get_user(req, resp, *, id):
resp.media = {"id": id}
"""
return RouteGroup(api=self, prefix=prefix)
async def __call__(self, scope, receive, send):
await self.app(scope, receive, send)
class RouteGroup:
"""A group of routes with a shared URL prefix."""
def __init__(self, api, prefix):
self.api = api
self.prefix = prefix.rstrip("/")
def route(self, route=None, **options):
full_route = f"{self.prefix}{route}"
return self.api.route(full_route, **options)
def before_request(self, **kwargs):
return self.api.before_request(**kwargs)
+82
View File
@@ -0,0 +1,82 @@
import asyncio
import concurrent.futures
import inspect
import multiprocessing
import traceback
from starlette.concurrency import run_in_threadpool
__all__ = ["BackgroundQueue"]
class BackgroundQueue:
"""A queue for running tasks in background threads.
Uses a ``ThreadPoolExecutor`` sized to the number of CPUs. Access it
via ``api.background``.
Usage::
# As a decorator — fire and forget
@api.background.task
def send_email(to, subject):
...
send_email("user@example.com", "Hello")
# Direct submission
future = api.background.run(send_email, "user@example.com", "Hello")
# As a callable (supports async functions)
await api.background(send_email, "user@example.com", "Hello")
"""
def __init__(self, n=None):
"""Create a new background queue.
:param n: Number of worker threads. Defaults to CPU count.
"""
if n is None:
n = multiprocessing.cpu_count()
self.n = n
self.pool = concurrent.futures.ThreadPoolExecutor(max_workers=n)
self.results = []
def run(self, f, *args, **kwargs):
"""Submit a function to run in a background thread.
:param f: The function to run.
:returns: A ``concurrent.futures.Future`` for the result.
"""
f = self.pool.submit(f, *args, **kwargs)
self.results.append(f)
return f
def task(self, f):
"""Decorator that wraps a function to run in the background thread pool.
The decorated function returns a ``Future`` instead of blocking.
Exceptions are printed to stderr via traceback.
:param f: The function to wrap.
"""
def on_future_done(fs):
try:
fs.result()
except Exception:
traceback.print_exc()
def do_task(*args, **kwargs):
result = self.run(f, *args, **kwargs)
result.add_done_callback(on_future_done)
return result
return do_task
async def __call__(self, func, *args, **kwargs) -> None:
if inspect.iscoroutinefunction(func):
return await asyncio.create_task(func(*args, **kwargs))
return await run_in_threadpool(func, *args, **kwargs)
+7 -2
View File
@@ -1,3 +1,8 @@
from .api import API
from . import status
from .views import GraphQLSchema
from .models import Request, Response
__all__ = [
"API",
"Request",
"Response",
]
+129
View File
@@ -0,0 +1,129 @@
"""
Responder CLI.
A web framework for Python.
Commands:
run Start the application server
build Build frontend assets using npm
Usage:
responder
responder run [--debug] [--limit-max-requests=] <target>
responder build [<target>]
responder --version
Options:
-h --help Show this screen.
-v --version Show version.
--debug Enable debug mode with verbose logging.
--limit-max-requests=<n> Maximum number of requests to handle before shutting down.
Arguments:
<target> For run: Python module specifier (e.g., "app:api" loads api from app.py)
Format: "module.submodule:variable_name" where variable_name is your API instance
For build: Directory containing package.json (default: current directory)
Examples:
responder run app:api # Run the 'api' instance from app.py
responder run myapp/core.py:application # Run the 'application' instance from myapp/core.py
responder build # Build frontend assets
""" # noqa: E501
import logging
import platform
import subprocess
import sys
import typing as t
from pathlib import Path
import docopt
from responder.__version__ import __version__
from responder.util.python import InvalidTarget, load_target
logger = logging.getLogger(__name__)
def cli() -> None:
"""
Main entry point for the Responder CLI.
Parses command line arguments and executes the appropriate command.
Supports running the application, building assets, and displaying version info.
"""
args = docopt.docopt(__doc__, argv=None, version=__version__, options_first=False)
setup_logging(args["--debug"])
target: t.Optional[str] = args["<target>"]
build: bool = args["build"]
debug: bool = args["--debug"]
run: bool = args["run"]
if build:
target_path = Path(target).resolve() if target else Path.cwd()
if not target_path.is_dir() or not (target_path / "package.json").exists():
logger.error(
f"Invalid target directory or missing package.json: {target_path}"
)
sys.exit(1)
npm_cmd = "npm.cmd" if platform.system() == "Windows" else "npm"
try:
logger.info("Starting frontend asset build")
# S603, S607 are addressed by validating the target directory.
subprocess.check_call( # noqa: S603, S607
[npm_cmd, "run", "build"],
cwd=target_path,
timeout=300,
)
logger.info("Frontend asset build completed successfully")
except FileNotFoundError:
logger.error("npm not found. Please install Node.js and npm.")
sys.exit(1)
except subprocess.CalledProcessError as e:
logger.error(f"Build failed with exit code {e.returncode}")
sys.exit(1)
if run:
if not target:
logger.error("Target argument is required for run command")
sys.exit(1)
# Maximum request limit. Terminating afterward. Suitable for software testing.
limit_max_requests = args["--limit-max-requests"]
if limit_max_requests is not None:
try:
limit_max_requests = int(limit_max_requests)
if limit_max_requests <= 0:
logger.error("limit-max-requests must be a positive integer")
sys.exit(1)
except ValueError:
logger.error("limit-max-requests must be a valid integer")
sys.exit(1)
# Load application from target.
try:
api = load_target(target=target)
except InvalidTarget as ex:
raise ValueError(
f"{ex}. "
"Use either a Python module entrypoint specification, "
"a filesystem path, or a remote URL. "
"See also https://responder.kennethreitz.org/cli.html."
) from ex
# Launch Responder API server (uvicorn).
api.run(debug=debug, limit_max_requests=limit_max_requests)
def setup_logging(debug: bool) -> None:
"""
Configure logging based on debug mode.
Args:
debug: When True, sets logging level to DEBUG; otherwise, sets to INFO
"""
log_level = logging.DEBUG if debug else logging.INFO
logging.basicConfig(
level=log_level, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
+111
View File
@@ -0,0 +1,111 @@
import json
from .templates import GRAPHIQL
class GraphQLView:
"""A class-based view that serves a GraphQL API.
Handles query resolution from multiple sources (JSON body, query
parameters, raw request text) and renders the GraphiQL IDE for
browser requests.
:param api: The Responder API instance.
:param schema: A Graphene schema instance.
"""
def __init__(self, *, api, schema):
self.api = api
self.schema = schema
@staticmethod
def _parse_variables(raw):
"""Parse variables from a string (query param) or return as-is (dict)."""
if raw is None:
return None
if isinstance(raw, str):
try:
return json.loads(raw)
except (json.JSONDecodeError, ValueError):
return None
return raw
@staticmethod
async def _resolve_graphql_query(req, resp):
"""Extract query, variables, and operationName from the request.
Supports multiple input sources, checked in order:
1. JSON body (``Content-Type: application/json``)
2. Form data (``Content-Type: application/x-www-form-urlencoded``)
3. Query parameters (``?query=...&variables=...&operationName=...``)
4. Raw request text
"""
if "json" in req.mimetype:
json_media = await req.media("json")
if "query" not in json_media:
resp.status_code = 400
resp.media = {"errors": ["'query' key is required in the JSON payload"]}
return None, None, None
return (
json_media["query"],
json_media.get("variables"),
json_media.get("operationName"),
)
if "form" in req.mimetype:
form_data = await req.media("form")
if "query" in form_data:
return (
form_data["query"],
GraphQLView._parse_variables(form_data.get("variables")),
form_data.get("operationName"),
)
# Support query/variables/operationName in query params.
if "query" in req.params:
return (
req.params["query"],
GraphQLView._parse_variables(req.params.get("variables")),
req.params.get("operationName"),
)
if "q" in req.params:
return req.params["q"], None, None
# Otherwise, the request text is used (typical).
return await req.text, None, None
async def graphql_response(self, req, resp):
"""Process a GraphQL request and populate the response."""
show_graphiql = req.method == "get" and req.accepts("text/html")
if show_graphiql:
resp.content = self.api.templates.render_string(
GRAPHIQL, endpoint=req.url.path
)
return None
query, variables, operation_name = await self._resolve_graphql_query(req, resp)
if query is None:
return None
context = {"request": req, "response": resp}
result = self.schema.execute(
query, variables=variables, operation_name=operation_name, context=context
)
response_data = {}
if result.errors:
response_data["errors"] = [{"message": str(e)} for e in result.errors]
if result.data is not None:
response_data["data"] = result.data
resp.media = response_data
status_code = 200 if not result.errors else 400
return (query, json.dumps(response_data), status_code)
async def on_request(self, req, resp):
await self.graphql_response(req, resp)
async def __call__(self, req, resp):
await self.on_request(req, resp)
+34
View File
@@ -0,0 +1,34 @@
# ruff: noqa: E501
GRAPHIQL = """
{% set GRAPHIQL_VERSION = '3.0.6' %}
{% set REACT_VERSION = '18.2.0' %}
<!DOCTYPE html>
<html>
<head>
<style>
body {
height: 100%;
margin: 0;
width: 100%;
overflow: hidden;
}
#graphiql {
height: 100vh;
}
</style>
<link href="//cdn.jsdelivr.net/npm/graphiql@{{ GRAPHIQL_VERSION }}/graphiql.min.css" rel="stylesheet"/>
</head>
<body>
<div id="graphiql">Loading...</div>
<script crossorigin src="//cdn.jsdelivr.net/npm/react@{{ REACT_VERSION }}/umd/react.production.min.js"></script>
<script crossorigin src="//cdn.jsdelivr.net/npm/react-dom@{{ REACT_VERSION }}/umd/react-dom.production.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/graphiql@{{ GRAPHIQL_VERSION }}/graphiql.min.js"></script>
<script>
const fetcher = GraphiQL.createFetcher({ url: '{{ endpoint }}' });
const root = ReactDOM.createRoot(document.getElementById('graphiql'));
root.render(React.createElement(GraphiQL, { fetcher: fetcher }));
</script>
</body>
</html>
""".strip()
+233
View File
@@ -0,0 +1,233 @@
from pathlib import Path
from apispec import APISpec, yaml_utils
from apispec.ext.marshmallow import MarshmallowPlugin
from responder import status_codes
from responder.statics import API_THEMES, DEFAULT_OPENAPI_THEME
from responder.templates import Templates
def _is_pydantic_model(obj):
"""Check if obj is a Pydantic model class."""
try:
from pydantic import BaseModel
return isinstance(obj, type) and issubclass(obj, BaseModel)
except ImportError:
return False
class PydanticPlugin:
"""APISpec plugin that resolves Pydantic models to JSON Schema."""
def __init__(self):
self._schemas = {}
def definition_helper(self, name, definition, **kwargs):
schema = kwargs.get("schema")
if schema is not None and _is_pydantic_model(schema):
return schema.model_json_schema()
return None
def resolve_schemas(self, spec):
pass
def init_spec(self, spec):
pass
def operation_helper(self, **kwargs):
return {}
class OpenAPISchema:
def __init__(
self,
app,
title,
version,
plugins=None,
description=None,
terms_of_service=None,
contact=None,
license=None, # noqa: A002
openapi=None,
openapi_route="/schema.yml",
docs_route="/docs/",
static_route="/static",
openapi_theme=DEFAULT_OPENAPI_THEME,
):
self.app = app
self.schemas = {}
self.pydantic_schemas = {}
self.title = title
self.version = version
self.description = description
self.terms_of_service = terms_of_service
self.contact = contact
self.license = license
self.openapi_version = openapi
self.openapi_route = openapi_route
self.docs_theme = (
openapi_theme if openapi_theme in API_THEMES else DEFAULT_OPENAPI_THEME
)
self.docs_route = docs_route
self.plugins = [MarshmallowPlugin()] if plugins is None else plugins
if self.openapi_version is not None:
self.app.add_route(self.openapi_route, self.schema_response)
if self.docs_route is not None:
self.app.add_route(self.docs_route, self.docs_response)
theme_path = (Path(__file__).parent / "docs").resolve()
self.templates = Templates(directory=theme_path)
self.static_route = static_route
@property
def _apispec(self):
info = {}
if self.description is not None:
info["description"] = self.description
if self.terms_of_service is not None:
info["termsOfService"] = self.terms_of_service
if self.contact is not None:
info["contact"] = self.contact
if self.license is not None:
info["license"] = self.license
spec = APISpec(
title=self.title,
version=self.version,
openapi_version=self.openapi_version,
plugins=self.plugins,
info=info,
)
for route in self.app.router.routes:
if route.description:
operations = yaml_utils.load_operations_from_docstring(route.description)
spec.path(path=route.route, operations=operations)
# Check for Pydantic-annotated routes
endpoint = route.endpoint
req_model = getattr(endpoint, "_request_model", None)
resp_model = getattr(endpoint, "_response_model", None)
if req_model or resp_model:
operations = {}
methods = getattr(route, "methods", None) or ["get"]
for method in [m.lower() for m in methods]:
op = {}
if req_model and method in ("post", "put", "patch"):
model_name = req_model.__name__
op["requestBody"] = {
"content": {
"application/json": {
"schema": {
"$ref": f"#/components/schemas/{model_name}"
}
}
}
}
if resp_model:
model_name = resp_model.__name__
op["responses"] = {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"$ref": f"#/components/schemas/{model_name}"
}
}
},
}
}
if op:
operations[method] = op
if operations and not route.description:
spec.path(path=route.route, operations=operations)
# Register marshmallow schemas
for name, schema in self.schemas.items():
spec.components.schema(name, schema=schema)
# Register Pydantic schemas
for name, model in self.pydantic_schemas.items():
json_schema = model.model_json_schema()
json_schema.pop("title", None)
spec.components.schema(name, component=json_schema)
return spec
@property
def openapi(self):
return self._apispec.to_yaml()
def add_schema(self, name, schema, check_existing=True):
"""Adds a marshmallow or Pydantic schema to the API specification."""
if check_existing:
assert name not in self.schemas
assert name not in self.pydantic_schemas
if _is_pydantic_model(schema):
self.pydantic_schemas[name] = schema
else:
self.schemas[name] = schema
def schema(self, name, **options):
"""Decorator for registering schemas (marshmallow or Pydantic).
Usage::
from marshmallow import Schema, fields
@api.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
Or with Pydantic::
from pydantic import BaseModel
@api.schema("Pet")
class Pet(BaseModel):
name: str
age: int = 0
"""
def decorator(f):
self.add_schema(name=name, schema=f, **options)
return f
return decorator
@property
def docs(self):
return self.templates.render(
f"{self.docs_theme}.html",
title=self.title,
version=self.version,
schema_url="/schema.yml",
)
def static_url(self, asset):
"""Given a static asset, return its URL path."""
assert self.static_route is not None
return f"{self.static_route}/{str(asset)}"
def docs_response(self, req, resp):
resp.html = self.docs
def schema_response(self, req, resp):
resp.status_code = status_codes.HTTP_200 # type: ignore[attr-defined]
resp.headers["Content-Type"] = "application/x-yaml"
resp.content = self.openapi
+24
View File
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<title>{{ title }} {{ version }}</title>
<!-- Embed elements Elements via Web Component -->
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
<link
rel="stylesheet"
href="https://unpkg.com/@stoplight/elements/styles.min.css"
/>
</head>
<body>
<elements-api
apiDescriptionUrl="{{ schema_url }}"
router="hash"
layout="sidebar"
/>
</body>
</html>
+16
View File
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<!-- Important: must specify -->
<html>
<head>
<title>{{ title }} {{ version }}</title>
<meta charset="utf-8" />
<!-- Important: rapi-doc uses utf8 characters -->
<script
type="module"
src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"
></script>
</head>
<body>
<rapi-doc spec-url="{{ schema_url }}" show-header="false"> </rapi-doc>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title>{{ title }} {{ version }}</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700"
rel="stylesheet"
/>
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url="{{ schema_url }}"></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"></script>
</body>
</html>
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>{{ title }} {{ version }}</title>
<link
rel="stylesheet"
type="text/css"
href="https://unpkg.com/swagger-ui-dist/swagger-ui.css"
/>
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = function () {
const ui = SwaggerUIBundle({
url: "{{ schema_url }}",
dom_id: "#swagger-ui",
deepLinking: true,
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
layout: "BaseLayout",
});
};
</script>
</body>
</html>
+65
View File
@@ -0,0 +1,65 @@
"""Simple in-memory rate limiter for Responder."""
import time
from collections import defaultdict
class RateLimiter:
"""Token bucket rate limiter.
Usage::
from responder.ext.ratelimit import RateLimiter
limiter = RateLimiter(requests=100, period=60) # 100 req/min
@api.route(before_request=True)
def rate_limit(req, resp):
limiter.check(req, resp)
Or use the shorthand::
limiter = RateLimiter(requests=100, period=60)
limiter.install(api)
"""
def __init__(self, requests=100, period=60):
self.max_requests = requests
self.period = period
self._buckets: dict[str, list[float]] = defaultdict(list)
def _client_key(self, req):
client = req.client
if client:
return client[0]
return req.headers.get("X-Forwarded-For", "unknown")
def _cleanup(self, key):
now = time.time()
cutoff = now - self.period
self._buckets[key] = [t for t in self._buckets[key] if t > cutoff]
def check(self, req, resp):
"""Check rate limit. Sets 429 status if exceeded."""
key = self._client_key(req)
self._cleanup(key)
if len(self._buckets[key]) >= self.max_requests:
resp.status_code = 429
resp.media = {"error": "rate limit exceeded"}
resp.headers["Retry-After"] = str(self.period)
return False
self._buckets[key].append(time.time())
remaining = self.max_requests - len(self._buckets[key])
resp.headers["X-RateLimit-Limit"] = str(self.max_requests)
resp.headers["X-RateLimit-Remaining"] = str(remaining)
return True
def install(self, api):
"""Install as a before_request hook on the API."""
@api.route(before_request=True)
def _rate_limit(req, resp):
self.check(req, resp)
+163
View File
@@ -0,0 +1,163 @@
from __future__ import annotations
import json
from urllib.parse import urlencode
import yaml
from python_multipart import MultipartParser
from .models import QueryDict
class _PartData:
__slots__ = ("headers", "body", "header_field")
def __init__(self):
self.headers: dict[str, str] = {}
self.body = b""
self.header_field = ""
def _parse_multipart(content: bytes, content_type: str) -> list[_PartData]:
"""Parse multipart form data into a list of parts with headers and body."""
boundary = None
for segment in content_type.split(";"):
segment = segment.strip()
if segment.startswith("boundary="):
boundary = segment.split("=", 1)[1].strip('"')
break
if boundary is None:
return []
parts: list[_PartData] = []
current: list[_PartData | None] = [None]
def on_part_begin():
current[0] = _PartData()
def on_part_data(data, start, end):
current[0].body += data[start:end] # type: ignore[union-attr]
def on_header_field(data, start, end):
current[0].header_field = data[start:end].decode("utf-8") # type: ignore[union-attr]
def on_header_value(data, start, end):
part = current[0]
assert part is not None
part.headers[part.header_field] = data[start:end].decode("utf-8")
def on_part_end():
parts.append(current[0]) # type: ignore[arg-type]
parser = MultipartParser(
boundary.encode(),
{ # type: ignore[arg-type]
"on_part_begin": on_part_begin,
"on_part_data": on_part_data,
"on_header_field": on_header_field,
"on_header_value": on_header_value,
"on_part_end": on_part_end,
},
)
parser.write(content)
parser.finalize()
return parts
async def format_form(r, encode=False):
if encode:
return None
if "multipart/form-data" in r.headers.get("Content-Type"):
parts = _parse_multipart(await r.content, r.mimetype)
queries = []
for part in parts:
header = part.headers.get("Content-Disposition", "")
text = part.body.decode("utf-8")
for section in [h.strip() for h in header.split(";")]:
split = section.split("=")
if len(split) > 1:
key = split[1]
key = key[1:-1]
queries.append((key, text))
content = urlencode(queries)
return QueryDict(content)
return QueryDict(await r.text)
async def format_yaml(r, encode=False):
if encode:
r.headers.update({"Content-Type": "application/x-yaml"})
return yaml.safe_dump(r.media)
return yaml.safe_load(await r.content)
async def format_json(r, encode=False):
if encode:
r.headers.update({"Content-Type": "application/json"})
return json.dumps(r.media)
return json.loads(await r.content)
async def format_files(r, encode=False):
if encode:
return None
parts = _parse_multipart(await r.content, r.mimetype)
dump = {}
for part in parts:
header = part.headers.get("Content-Disposition", "")
mimetype = part.headers.get("Content-Type", None)
filename = None
formname = None
for section in [h.strip() for h in header.split(";")]:
split = section.split("=")
if len(split) > 1:
key = split[0]
value = split[1]
value = value[1:-1]
if key == "filename":
filename = value
elif key == "name":
formname = value
if formname is None:
continue
if mimetype is None:
dump[formname] = part.body
else:
dump[formname] = {
"filename": filename,
"content": part.body,
"content-type": mimetype,
}
return dump
async def format_msgpack(r, encode=False):
try:
import msgpack
except ImportError as exc:
raise ImportError(
"msgpack is required for MessagePack support: pip install msgpack"
) from exc
if encode:
r.headers.update({"Content-Type": "application/x-msgpack"})
return msgpack.packb(r.media)
return msgpack.unpackb(await r.content)
def get_formats():
return {
"json": format_json,
"yaml": format_yaml,
"form": format_form,
"files": format_files,
"msgpack": format_msgpack,
}
+578 -103
View File
@@ -1,137 +1,612 @@
import io
import json
import gzip
from __future__ import annotations
import graphene
import yaml
from requests.models import Request as RequestsRequest
from requests.structures import CaseInsensitiveDict
from werkzeug.wrappers import Request as WerkzeugRequest
from werkzeug.wrappers import BaseResponse as WerkzeugResponse
import functools
import inspect
from collections.abc import Callable
from http.cookies import SimpleCookie
from urllib.parse import parse_qs, urlparse
__all__ = ["Request", "Response", "QueryDict"]
try:
import chardet
except ImportError:
chardet = None # type: ignore[assignment]
from starlette.requests import Request as StarletteRequest
from starlette.requests import State
from starlette.responses import (
Response as StarletteResponse,
)
from starlette.responses import (
StreamingResponse as StarletteStreamingResponse,
)
from .statics import DEFAULT_ENCODING
from .status_codes import HTTP_301 # type: ignore[attr-defined]
from urllib.parse import parse_qs
class CaseInsensitiveDict(dict):
"""A case-insensitive dict for HTTP headers."""
from .status import HTTP_200
def __setitem__(self, key, value):
super().__setitem__(key.lower(), value)
# @staticmethod
# def funcname(parameter_list):
# pass
def __getitem__(self, key):
return super().__getitem__(key.lower())
def __contains__(self, key):
return super().__contains__(key.lower())
def get(self, key, default=None):
return super().get(key.lower(), default)
def update(self, other=None, **kwargs):
if other:
for key, value in other.items():
self[key] = value
for key, value in kwargs.items():
self[key] = value
class QueryDict(dict):
"""A dictionary for query string parameters that handles multi-value keys.
Single-value access returns the last value for a key. Use :meth:`get_list`
to retrieve all values for a multi-value parameter.
"""
def __init__(self, query_string):
self.update(parse_qs(query_string))
def __getitem__(self, key):
"""
Return the last data value for this key, or [] if it's an empty list;
raise KeyError if not found.
"""
list_ = super().__getitem__(key)
try:
return list_[-1]
except IndexError:
return []
def get(self, key, default=None):
"""
Return the last data value for the passed key. If key doesn't exist
or value is an empty list, return `default`.
"""
try:
val = self[key]
except KeyError:
return default
if val == []:
return default
return val
def _get_list(self, key, default=None, force_list=False):
"""
Return a list of values for the key.
Used internally to manipulate values list. If force_list is True,
return a new copy of values.
"""
try:
values = super().__getitem__(key)
except KeyError:
if default is None:
return []
return default
else:
if force_list:
values = list(values) if values is not None else None
return values
def get_list(self, key, default=None):
"""
Return the list of values for the key. If key doesn't exist, return a
default value.
"""
return self._get_list(key, default, force_list=True)
def items(self):
"""
Yield (key, value) pairs, where value is the last item in the list
associated with the key.
"""
for key in self:
yield key, self[key]
def items_list(self):
"""
Yield (key, value) pairs, where value is the the list.
"""
yield from super().items()
class Request:
def __init__(self):
super().__init__()
self._wz = None
"""An HTTP request, passed to each view as the first argument.
@classmethod
def from_environ(kls, environ):
self = kls()
self._wz = WerkzeugRequest(environ)
self.headers = CaseInsensitiveDict(self._wz.headers.to_list())
self.method = self._wz.method
self.full_url = self._wz.url
self.url = self._wz.base_url
self.full_path = self._wz.full_path
self.path = self._wz.path
self.params = parse_qs(self._wz.query_string.decode("utf-8"))
self.raw = self._wz.stream
self.content = self._wz.get_data(cache=True, as_text=False)
self.text = self._wz.get_data(cache=True, as_text=True)
self.data = self._wz.get_data(cache=True, as_text=True, parse_form_data=True)
Provides access to headers, cookies, query parameters, the request body,
session data, and more. Most properties are synchronous; reading the body
(via :attr:`content`, :attr:`text`, or :meth:`media`) requires ``await``.
"""
return self
__slots__ = [
"_starlette",
"formats",
"_headers",
"_encoding",
"api",
"_content",
"_cookies",
]
def __init__(self, scope, receive, api=None, formats=None):
self._starlette = StarletteRequest(scope, receive)
self.formats = formats
self._encoding = None
self.api = api
self._content = None
headers: CaseInsensitiveDict = CaseInsensitiveDict()
for key, value in self._starlette.headers.items():
headers[key] = value
self._headers = headers
self._cookies = None
@property
def accepts_yaml(self):
return "yaml" in self.headers["accept"]
def session(self):
"""The session data, in dict form, from the Request."""
return self._starlette.session
@property
def accepts_json(self):
return "json" in self.headers["accept"]
def headers(self):
"""A case-insensitive dictionary, containing all headers sent in the Request."""
return self._headers
def json(self):
return json.loads(self.content)
@property
def mimetype(self):
"""The MIME type of the request body, from the ``Content-Type`` header."""
return self.headers.get("Content-Type", "")
def yaml(self):
return yaml.load(self.content)
@property
def is_json(self):
"""Returns ``True`` if the request content type is JSON."""
return "json" in self.mimetype
@property
def method(self):
"""The incoming HTTP method used for the request, lower-cased."""
return self._starlette.method.lower()
@property
def full_url(self):
"""The full URL of the Request, query parameters and all."""
return str(self._starlette.url)
@property
def url(self):
"""The parsed URL of the Request."""
return urlparse(self.full_url)
@property
def cookies(self):
"""The cookies sent in the Request, as a dictionary."""
if self._cookies is None:
cookies = {}
cookie_header = self.headers.get("Cookie", "")
bc: SimpleCookie = SimpleCookie(cookie_header)
for key, morsel in bc.items():
cookies[key] = morsel.value
self._cookies = cookies
return self._cookies
@property
def params(self):
"""A dictionary of the parsed query parameters used for the Request."""
try:
return QueryDict(self.url.query)
except AttributeError:
return QueryDict({})
@property
def path_params(self) -> dict:
"""The path parameters extracted from the URL route."""
return self._starlette.path_params
@property
def client(self):
"""The client's address as a (host, port) named tuple, or None."""
return self._starlette.client
@property
def state(self) -> State:
"""
Use the state to store additional information.
This can be a very helpful feature, if you want to hand over
information from a middelware or a route decorator to the
actual route handler.
Usage: ``request.state.time_started = time.time()``
"""
return self._starlette.state
@property
async def encoding(self):
"""The encoding of the Request's body. Can be set, manually. Must be awaited."""
# Use the user-set encoding first.
if self._encoding:
return self._encoding
return await self.apparent_encoding
@encoding.setter
def encoding(self, value):
self._encoding = value
@property
async def content(self):
"""The Request body, as bytes. Must be awaited."""
if not self._content:
self._content = await self._starlette.body()
return self._content
@property
async def text(self):
"""The Request body, as unicode. Must be awaited."""
return (await self.content).decode(await self.encoding)
@property
async def declared_encoding(self):
if "Encoding" in self.headers:
return self.headers["Encoding"]
return None
@property
async def apparent_encoding(self):
"""The apparent encoding, detected automatically. Must be awaited.
Uses chardet for detection if installed, otherwise falls back to UTF-8.
"""
declared_encoding = await self.declared_encoding
if declared_encoding:
return declared_encoding
if chardet is not None:
return chardet.detect(await self.content)["encoding"] or DEFAULT_ENCODING
return DEFAULT_ENCODING
@property
def is_secure(self):
"""``True`` if the request was made over HTTPS."""
return self.url.scheme == "https"
def accepts(self, content_type):
"""Returns ``True`` if the incoming Request accepts the given ``content_type``."""
return content_type in self.headers.get("Accept", [])
async def media(self, format: str | Callable = None): # noqa: A002
"""Renders incoming json/yaml/form data as Python objects. Must be awaited.
:param format: The name of the format being used.
Alternatively, accepts a custom callable for the format type.
"""
if format is None:
format = "yaml" if "yaml" in self.mimetype or "" else "json" # noqa: A001
format = "form" if "form" in self.mimetype or "" else format # noqa: A001
formatter: Callable
if isinstance(format, str):
try:
formatter = self.formats[format]
except KeyError as ex:
raise ValueError(f"Unable to process data in '{format}' format") from ex
elif callable(format):
formatter = format
else:
raise TypeError(f"Invalid 'format' argument: {format}")
return await formatter(self)
def content_setter(mimetype):
def getter(instance):
return instance.content
def setter(instance, value):
instance.content = value
instance.mimetype = mimetype
return property(fget=getter, fset=setter)
class Response:
def __init__(self, req):
"""An HTTP response, passed to each view as the second argument.
Mutate this object to control what gets sent back to the client. Set
:attr:`text`, :attr:`html`, :attr:`media`, or :attr:`content` to define
the body. Use :attr:`headers` and :meth:`set_cookie` to control metadata.
:var text: Set the response body as plain text (sets ``Content-Type: text/plain``).
:var html: Set the response body as HTML (sets ``Content-Type: text/html``).
:var media: Set a Python object (dict, list) to be serialized as JSON (or negotiated format).
:var content: Set the raw response body as bytes.
:var status_code: The HTTP status code (e.g. ``200``, ``404``). Defaults to ``200`` if not set.
:var headers: A dict of response headers.
:var cookies: A ``SimpleCookie`` holding cookies to set on the response.
:var session: A dict of session data. Changes are persisted in a signed cookie.
""" # noqa: E501
__slots__ = [
"req",
"status_code",
"content",
"encoding",
"media",
"headers",
"formats",
"cookies",
"session",
"mimetype",
"_stream",
]
text = content_setter("text/plain")
html = content_setter("text/html")
def __init__(self, req, *, formats):
self.req = req
self.status_code = HTTP_200
self.text = None
self.status_code: int | None = None
self.content = None
self.encoding = None
self.media = None
self.mimetype = None
self.encoding = DEFAULT_ENCODING
self.media = None
self._stream = None
self.headers = {}
self.formats = formats
self.cookies: SimpleCookie = SimpleCookie()
self.session = req.session
def stream(self, func, *args, **kwargs):
"""Set up a streaming response from an async generator function.
The generator yields chunks of bytes that are sent to the client
as they are produced, without buffering the full response in memory.
Usage::
@api.route("/stream")
async def stream_data(req, resp):
@resp.stream
async def body():
for i in range(10):
yield f"chunk {i}\\n".encode()
:param func: An async generator function that yields response chunks.
"""
assert inspect.isasyncgenfunction(func)
self._stream = functools.partial(func, *args, **kwargs)
return func
def sse(self, func, *args, **kwargs):
"""Set up Server-Sent Events streaming.
Usage::
@api.route("/events")
async def events(req, resp):
@resp.sse
async def stream():
for i in range(10):
yield {"data": f"message {i}"}
Each yielded dict can have: data, event, id, retry.
Yielding a string is treated as data.
"""
assert inspect.isasyncgenfunction(func)
async def sse_generator():
async for event in func(*args, **kwargs):
if isinstance(event, str):
yield f"data: {event}\n\n".encode()
elif isinstance(event, dict):
parts = []
if "event" in event:
parts.append(f"event: {event['event']}")
if "id" in event:
parts.append(f"id: {event['id']}")
if "retry" in event:
parts.append(f"retry: {event['retry']}")
data = event.get("data", "")
for line in str(data).split("\n"):
parts.append(f"data: {line}")
parts.append("")
parts.append("")
yield "\n".join(parts).encode()
else:
yield f"data: {event}\n\n".encode()
self._stream = sse_generator
self.mimetype = "text/event-stream"
self.headers["Cache-Control"] = "no-cache"
self.headers["Connection"] = "keep-alive"
return func
def stream_file(self, path, *, content_type=None, chunk_size=8192):
"""Stream a file without loading it entirely into memory.
:param path: Path to the file.
:param content_type: Optional MIME type override.
:param chunk_size: Size of chunks to read (default 8192 bytes).
"""
from pathlib import Path as PathType
path = PathType(path)
if content_type:
self.mimetype = content_type
else:
import mimetypes
guessed = mimetypes.guess_type(str(path))[0]
self.mimetype = guessed or "application/octet-stream"
async def file_generator():
with open(path, "rb") as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
yield chunk
self._stream = file_generator
def file(self, path, *, content_type=None):
"""Serve a file from disk as the response.
:param path: Path to the file to serve.
:param content_type: Optional MIME type override.
"""
from pathlib import Path
path = Path(path)
self.content = path.read_bytes()
if content_type:
self.mimetype = content_type
else:
import mimetypes
guessed = mimetypes.guess_type(str(path))[0]
self.mimetype = guessed or "application/octet-stream"
def redirect(self, location, *, set_text=True, status_code=HTTP_301):
"""Redirect the client to a different URL.
:param location: The URL to redirect to.
:param set_text: If ``True``, set a default redirect message as the body.
:param status_code: The HTTP status code (default ``301``).
"""
self.status_code = status_code
if set_text:
self.text = f"Redirecting to: {location}"
self.headers.update({"Location": location})
@property
def body(self):
async def body(self):
if self._stream is not None:
headers = {}
if self.mimetype is not None:
headers["Content-Type"] = self.mimetype
return (self._stream(), headers)
if self.content:
return (self.content, self.mimetype, {})
if self.content is not None:
headers = {}
content = self.content
if self.mimetype is not None:
headers["Content-Type"] = self.mimetype
if self.mimetype == "text/plain" and self.encoding is not None:
headers["Encoding"] = self.encoding
if isinstance(content, str):
content = content.encode(self.encoding)
return (content, headers)
if self.text:
return (
self.text.encode("utf-8"),
self.mimetype or "application/text",
{"Encoding": "utf-8"},
for format_ in self.formats:
if self.req.accepts(format_):
return (await self.formats[format_](self, encode=True)), {}
# Default to JSON anyway.
return (
await self.formats["json"](self, encode=True),
{"Content-Type": "application/json"},
)
def set_cookie(
self,
key,
value="",
expires=None,
path="/",
domain=None,
max_age=None,
secure=False,
httponly=True,
):
"""Set a cookie on the response with full control over directives.
:param key: The cookie name.
:param value: The cookie value.
:param expires: Expiration date string (e.g. ``"Thu, 01 Jan 2026 00:00:00 GMT"``).
:param path: URL path the cookie applies to (default ``"/"``).
:param domain: Domain the cookie is valid for.
:param max_age: Maximum age in seconds before the cookie expires.
:param secure: If ``True``, cookie is only sent over HTTPS.
:param httponly: If ``True`` (default), cookie is inaccessible to JavaScript.
Usage::
resp.set_cookie(
"token", value="abc123",
max_age=3600, secure=True, httponly=True,
)
if self.media:
if self.req.accepts_yaml:
return (
yaml.dump(self.media).encode("utf-8"),
self.mimetype or "application/x-yaml",
{},
)
if self.req.accepts_json:
return (json.dumps(self.media), self.mimetype or "application/json", {})
"""
self.cookies[key] = value
morsel = self.cookies[key]
if expires is not None:
morsel["expires"] = expires
if path is not None:
morsel["path"] = path
if domain is not None:
morsel["domain"] = domain
if max_age is not None:
morsel["max-age"] = max_age
morsel["secure"] = secure
morsel["httponly"] = httponly
@property
def gzipped_body(self):
body, mimetype, headers = self.body
# print(self.req.headers)
if "gzip" in self.req.headers["Accept-Encoding"].lower():
gzip_buffer = io.BytesIO()
gzip_file = gzip.GzipFile(mode="wb", fileobj=gzip_buffer)
gzip_file.write(body)
gzip_file.close()
new_headers = {
"Content-Encoding": "gzip",
"Vary": "Accept-Encoding",
"Content-Length": str(len(body)),
}
headers.update(new_headers)
# print(headers)
return (gzip_buffer.getvalue(), mimetype, headers)
else:
return (body, mimetype, headers)
@property
def _wz(self):
body, mimetype, headers = self.body
headers.update(self.headers)
r = WerkzeugResponse(
body,
status=self.status_code,
mimetype=self.mimetype or mimetype,
direct_passthrough=False,
def _prepare_cookies(self, starlette_response):
cookie_header = (
(b"set-cookie", morsel.output(header="").lstrip().encode("latin-1"))
for morsel in self.cookies.values()
)
r.headers = headers
return r
starlette_response.raw_headers.extend(cookie_header)
def __call__(self, environ, start_response):
return self._wz(environ, start_response)
async def __call__(self, scope, receive, send):
body, headers = await self.body
if self.headers:
headers.update(self.headers)
response_cls: type[StarletteResponse] | type[StarletteStreamingResponse]
if self._stream is not None:
response_cls = StarletteStreamingResponse
else:
response_cls = StarletteResponse
class Schema(graphene.Schema):
def on_request(self, req, resp):
pass
response = response_cls(body, status_code=self.status_code_safe, headers=headers)
self._prepare_cookies(response)
await response(scope, receive, send)
@property
def ok(self):
"""``True`` if the status code is in the 2xx range (success)."""
return 200 <= self.status_code_safe < 300
@property
def status_code_safe(self) -> int:
"""Return the status code, raising ``RuntimeError`` if it hasn't been set."""
if self.status_code is None:
raise RuntimeError("HTTP status code has not been defined")
return self.status_code
+472
View File
@@ -0,0 +1,472 @@
from __future__ import annotations
import asyncio
import inspect
import re
import traceback
from collections import defaultdict
from collections.abc import Callable
from typing import Any, Union
__all__ = ["Route", "WebSocketRoute", "Router"]
from starlette.concurrency import run_in_threadpool
from starlette.exceptions import HTTPException
from starlette.types import ASGIApp, Receive, Scope, Send
from starlette.websockets import WebSocket, WebSocketClose
from . import status_codes
from .formats import get_formats
from .models import Request, Response
_UUID_RE = r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
_CONVERTORS = {
"int": (int, r"\d+"),
"str": (str, r"[^/]+"),
"float": (float, r"\d+(.\d+)?"),
"path": (str, r".+"),
"uuid": (str, _UUID_RE),
}
PARAM_RE = re.compile("{([a-zA-Z_][a-zA-Z0-9_]*)(:[a-zA-Z_][a-zA-Z0-9_]*)?}")
def compile_path(path: str) -> tuple[re.Pattern, dict[str, type]]:
path_re = "^"
param_convertors: dict[str, type] = {}
idx = 0
for match in PARAM_RE.finditer(path):
param_name, convertor_type = match.groups(default="str")
convertor_type = convertor_type.lstrip(":")
assert convertor_type in _CONVERTORS.keys(), (
f"Unknown path convertor '{convertor_type}'"
)
convertor, convertor_re = _CONVERTORS[convertor_type]
path_re += path[idx : match.start()]
path_re += rf"(?P<{param_name}>{convertor_re})"
param_convertors[param_name] = convertor
idx = match.end()
path_re += path[idx:] + "$"
return re.compile(path_re), param_convertors
class BaseRoute:
def matches(self, scope: Scope) -> tuple[bool, dict]:
raise NotImplementedError()
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
raise NotImplementedError()
class Route(BaseRoute):
"""An HTTP route that maps a URL pattern to an endpoint.
Supports path parameters with type convertors (``{id:int}``, ``{slug:str}``,
``{pk:uuid}``, ``{value:float}``, ``{rest:path}``).
"""
def __init__(
self,
route: str,
endpoint: Callable,
*,
before_request: bool = False,
methods: list[str] | None = None,
) -> None:
assert route.startswith("/"), "Route path must start with '/'"
self.route = route
self.endpoint = endpoint
self.before_request = before_request
self.methods: set[str] | None = {m.upper() for m in methods} if methods else None
self.path_re: re.Pattern
self.param_convertors: dict[str, type]
self.path_re, self.param_convertors = compile_path(route)
# Strip type annotations for URL generation (e.g. {id:int} -> {id})
self._url_template = PARAM_RE.sub(r"{\1}", route)
def __repr__(self) -> str:
return f"<Route {self.route!r}={self.endpoint!r}>"
def url(self, **params: Any) -> str:
return self._url_template.format(**params)
@property
def endpoint_name(self) -> str:
return self.endpoint.__name__
@property
def description(self) -> str | None:
return self.endpoint.__doc__
def matches(self, scope: Scope) -> tuple[bool, dict]:
if scope["type"] != "http":
return False, {}
if self.methods and scope.get("method", "").upper() not in self.methods:
return False, {}
path = scope["path"]
match = self.path_re.match(path)
if match is None:
return False, {}
matched_params = match.groupdict()
for key, value in matched_params.items():
matched_params[key] = self.param_convertors[key](value)
return True, {"path_params": {**matched_params}}
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
request = Request(scope, receive, formats=get_formats())
response = Response(req=request, formats=get_formats())
path_params = scope.get("path_params", {})
before_requests = scope.get("before_requests", [])
for before_request in before_requests.get("http", []):
if inspect.iscoroutinefunction(before_request):
await before_request(request, response)
else:
await run_in_threadpool(before_request, request, response)
# If a before_request hook set a status code, short-circuit
if response.status_code is not None:
await response(scope, receive, send)
return
# Auto-validate request body with Pydantic model
req_model = getattr(self.endpoint, "_request_model", None)
if req_model is not None and request.method in ("post", "put", "patch"):
try:
body = await request.media()
req_model(**body)
except Exception as exc:
response.status_code = 422
errors = []
if hasattr(exc, "errors"):
errors = exc.errors()
else:
errors = [{"msg": str(exc)}]
response.media = {"errors": errors}
await response(scope, receive, send)
return
views = []
if inspect.isclass(self.endpoint):
endpoint = self.endpoint()
on_request = getattr(endpoint, "on_request", None)
if on_request:
views.append(on_request)
method_name = f"on_{request.method}"
try:
view = getattr(endpoint, method_name)
views.append(view)
except AttributeError as ex:
if on_request is None:
raise HTTPException(status_code=status_codes.HTTP_405) from ex # type: ignore[attr-defined]
else:
views.append(self.endpoint)
for view in views:
# Check __call__ for class-based views (e.g. GraphQL)
if inspect.iscoroutinefunction(view) or inspect.iscoroutinefunction(
view.__call__
):
await view(request, response, **path_params)
else:
await run_in_threadpool(view, request, response, **path_params)
# Auto-serialize response with Pydantic model
resp_model = getattr(self.endpoint, "_response_model", None)
if resp_model is not None and response.media is not None:
try:
validated = resp_model(**response.media)
response.media = validated.model_dump()
except (ValueError, TypeError):
pass # Don't break the response if serialization fails
# Run after-request hooks
after_requests = scope.get("after_requests", [])
for after_request in after_requests:
if inspect.iscoroutinefunction(after_request):
await after_request(request, response)
else:
await run_in_threadpool(after_request, request, response)
if response.status_code is None:
response.status_code = status_codes.HTTP_200 # type: ignore[attr-defined]
await response(scope, receive, send)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Route):
return NotImplemented
return self.route == other.route and self.endpoint == other.endpoint
def __hash__(self) -> int:
return hash(self.route) ^ hash(self.endpoint) ^ hash(self.before_request)
class WebSocketRoute(BaseRoute):
"""A WebSocket route that maps a URL pattern to a WebSocket handler."""
def __init__(
self, route: str, endpoint: Callable, *, before_request: bool = False
) -> None:
assert route.startswith("/"), "Route path must start with '/'"
self.route = route
self.endpoint = endpoint
self.before_request = before_request
self.path_re: re.Pattern
self.param_convertors: dict[str, type]
self.path_re, self.param_convertors = compile_path(route)
self._url_template = PARAM_RE.sub(r"{\1}", route)
def __repr__(self) -> str:
return f"<Route {self.route!r}={self.endpoint!r}>"
def url(self, **params: Any) -> str:
return self._url_template.format(**params)
@property
def endpoint_name(self) -> str:
return self.endpoint.__name__
@property
def description(self) -> str | None:
return self.endpoint.__doc__
def matches(self, scope: Scope) -> tuple[bool, dict]:
if scope["type"] != "websocket":
return False, {}
path = scope["path"]
match = self.path_re.match(path)
if match is None:
return False, {}
matched_params = match.groupdict()
for key, value in matched_params.items():
matched_params[key] = self.param_convertors[key](value)
return True, {"path_params": {**matched_params}}
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
ws = WebSocket(scope, receive, send)
before_requests = scope.get("before_requests", [])
for before_request in before_requests.get("ws", []):
await before_request(ws)
await self.endpoint(ws)
def __eq__(self, other: object) -> bool:
if not isinstance(other, WebSocketRoute):
return NotImplemented
return self.route == other.route and self.endpoint == other.endpoint
def __hash__(self) -> int:
return hash(self.route) ^ hash(self.endpoint) ^ hash(self.before_request)
class Router:
"""The core router that dispatches incoming requests to matching routes.
Handles route matching, before/after request hooks, lifespan events,
and mounted sub-applications.
"""
def __init__(
self,
routes: list[BaseRoute] | None = None,
default_response: Callable | None = None,
before_requests: dict[str, list[Callable]] | None = None,
lifespan: Callable | None = None,
) -> None:
self.routes: list[BaseRoute] = [] if routes is None else list(routes)
self.apps: dict[str, Union[ASGIApp, Any]] = {}
self.default_endpoint: Callable = (
self.default_response if default_response is None else default_response
)
self.before_requests: dict[str, list[Callable]] = (
{"http": [], "ws": []} if before_requests is None else before_requests
)
self.after_requests: list[Callable] = []
self.events: defaultdict[str, list[Callable]] = defaultdict(list)
self._lifespan_handler = lifespan
def add_route(
self,
route: str | None = None,
endpoint: Callable | None = None,
*,
default: bool = False,
websocket: bool = False,
before_request: bool = False,
check_existing: bool = False,
methods: list[str] | None = None,
) -> None:
"""Adds a route to the router.
:param route: A string representation of the route
:param endpoint: The endpoint for the route -- can be callable, or class.
:param default: If ``True``, all unknown requests will route to this view.
:param methods: Optional list of HTTP methods (e.g. ["GET", "POST"]).
"""
if before_request:
if websocket:
self.before_requests.setdefault("ws", []).append(endpoint)
else:
self.before_requests.setdefault("http", []).append(endpoint)
return
if check_existing:
assert not self.routes or route not in (item.route for item in self.routes), (
f"Route '{route}' already exists"
)
if default:
self.default_endpoint = endpoint
if websocket:
route = WebSocketRoute(route, endpoint)
else:
route = Route(route, endpoint, methods=methods)
self.routes.append(route)
def mount(self, route: str, app: Any) -> None:
"""Mounts ASGI / WSGI applications at a given route"""
self.apps.update({route: app})
def add_event_handler(self, event_type: str, handler: Callable) -> None:
assert event_type in (
"startup",
"shutdown",
), f"Only 'startup' and 'shutdown' events are supported, not {event_type}."
self.events[event_type].append(handler)
async def trigger_event(self, event_type: str) -> None:
for handler in self.events.get(event_type, []):
if inspect.iscoroutinefunction(handler):
await handler()
else:
handler()
def before_request(self, endpoint: Callable, websocket: bool = False) -> None:
if websocket:
self.before_requests.setdefault("ws", []).append(endpoint)
else:
self.before_requests.setdefault("http", []).append(endpoint)
def after_request(self, endpoint: Callable) -> None:
self.after_requests.append(endpoint)
def url_for(self, endpoint: Callable | str, **params: Any) -> str | None:
for route in self.routes:
if endpoint in (route.endpoint, route.endpoint.__name__):
return route.url(**params)
return None
async def default_response(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] == "websocket":
websocket_close = WebSocketClose()
await websocket_close(scope, receive, send)
return
request = Request(scope, receive)
response = Response(request, formats=get_formats()) # noqa: F841
raise HTTPException(status_code=status_codes.HTTP_404) # type: ignore[attr-defined]
def _resolve_route(self, scope: Scope) -> BaseRoute | None:
for route in self.routes:
matches, child_scope = route.matches(scope)
if matches:
scope.update(child_scope)
return route
return None
async def lifespan(self, scope: Scope, receive: Receive, send: Send) -> None:
message = await receive()
assert message["type"] == "lifespan.startup"
if self._lifespan_handler is not None:
# Modern lifespan context manager pattern
try:
ctx = self._lifespan_handler(scope.get("app"))
await ctx.__aenter__()
except BaseException:
msg = traceback.format_exc()
await send({"type": "lifespan.startup.failed", "message": msg})
raise
await send({"type": "lifespan.startup.complete"})
message = await receive()
assert message["type"] == "lifespan.shutdown"
await ctx.__aexit__(None, None, None)
else:
# Legacy on_event("startup") / on_event("shutdown") pattern
try:
await self.trigger_event("startup")
except BaseException:
msg = traceback.format_exc()
await send({"type": "lifespan.startup.failed", "message": msg})
raise
await send({"type": "lifespan.startup.complete"})
message = await receive()
assert message["type"] == "lifespan.shutdown"
await self.trigger_event("shutdown")
await send({"type": "lifespan.shutdown.complete"})
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
assert scope["type"] in ("http", "websocket", "lifespan")
if scope["type"] == "lifespan":
await self.lifespan(scope, receive, send)
return
path = scope["path"]
root_path = scope.get("root_path", "")
# Check "primary" mounted routes first (before submounted apps)
route = self._resolve_route(scope)
scope["before_requests"] = self.before_requests
scope["after_requests"] = self.after_requests
if route is not None:
await route(scope, receive, send)
return
# Call into a submounted app, if one exists.
for path_prefix, app in self.apps.items():
if path.startswith(path_prefix):
scope["path"] = path[len(path_prefix) :] or "/"
scope["root_path"] = root_path + path_prefix
try:
await app(scope, receive, send)
return
except TypeError:
from a2wsgi import WSGIMiddleware
app = WSGIMiddleware(app)
await app(scope, receive, send)
return
await self.default_endpoint(scope, receive, send)
+8
View File
@@ -0,0 +1,8 @@
from starlette.staticfiles import StaticFiles as StarletteStaticFiles
class StaticFiles(StarletteStaticFiles):
"""Extension to Starlette's StaticFiles with support for multiple directories."""
def add_directory(self, directory: str) -> None:
self.all_directories = [*self.all_directories, *self.get_directories(directory)]
+15
View File
@@ -0,0 +1,15 @@
API_THEMES = ["elements", "rapidoc", "redoc", "swagger_ui"]
DEFAULT_ENCODING = "utf-8"
DEFAULT_OPENAPI_THEME = "swagger_ui"
DEFAULT_SESSION_COOKIE = "Responder-Session"
DEFAULT_SECRET_KEY = "NOTASECRET" # noqa: S105
DEFAULT_CORS_PARAMS = {
"allow_origins": (),
"allow_methods": ("GET",),
"allow_headers": (),
"allow_credentials": False,
"allow_origin_regex": None,
"expose_headers": (),
"max_age": 600,
}
@@ -1,5 +1,3 @@
# from: https://github.com/requests/requests/blob/master/requests/status_codes.py
codes = {
# Informational.
100: ("continue",),
@@ -26,11 +24,7 @@ codes = {
305: ("use_proxy",),
306: ("switch_proxy",),
307: ("temporary_redirect", "temporary_moved", "temporary"),
308: (
"permanent_redirect",
"resume_incomplete",
"resume",
), # These 2 to be removed in 3.0
308: ("permanent_redirect",),
# Client Error.
400: ("bad_request", "bad"),
401: ("unauthorized",),
@@ -88,3 +82,27 @@ for number in codes:
for label in codes[number]:
locals()[label] = number
def _is_category(category, status_code):
return all([(status_code >= category), (status_code < category + 100)])
def is_100(status_code):
return _is_category(100, status_code)
def is_200(status_code):
return _is_category(200, status_code)
def is_300(status_code):
return _is_category(300, status_code)
def is_400(status_code):
return _is_category(400, status_code)
def is_500(status_code):
return _is_category(500, status_code)
+61
View File
@@ -0,0 +1,61 @@
from contextlib import contextmanager
import jinja2
__all__ = ["Templates"]
class Templates:
def __init__(
self, directory="templates", autoescape=True, context=None, enable_async=False
):
self.directory = directory
self._env = jinja2.Environment(
loader=jinja2.FileSystemLoader([str(self.directory)]),
autoescape=autoescape, # noqa: S701
enable_async=enable_async,
)
self.default_context = {} if context is None else {**context}
self._env.globals.update(self.default_context)
@property
def context(self):
return self._env.globals
@context.setter
def context(self, context):
self._env.globals = {**self.default_context, **context}
def get_template(self, name):
return self._env.get_template(name)
def render(self, template, *args, **kwargs):
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template, with provided values supplied.
:param template: The filename of the jinja2 template.
:param **kwargs: Data to pass into the template.
:param **kwargs: Data to pass into the template.
""" # noqa: E501
return self.get_template(template).render(*args, **kwargs)
@contextmanager
def _async(self):
self._env.is_async = True
try:
yield
finally:
self._env.is_async = False
async def render_async(self, template, *args, **kwargs):
with self._async():
return await self.get_template(template).render_async(*args, **kwargs)
def render_string(self, source, *args, **kwargs):
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template string, with provided values supplied.
:param source: The template to use.
:param *args, **kwargs: Data to pass into the template.
:param **kwargs: Data to pass into the template.
""" # noqa: E501
template = self._env.from_string(source)
return template.render(*args, **kwargs)
+242
View File
@@ -0,0 +1,242 @@
# ruff: noqa: S603 # Subprocess call - output not captured
# ruff: noqa: S607 # Starting a process with a partial executable path
# Security considerations for subprocess usage:
# 1. Only execute the 'responder' binary from PATH
# 2. Validate all user inputs before passing to subprocess
# 3. Use Path.resolve() to prevent path traversal
import functools
import logging
import os
import shutil
import signal
import socket
import subprocess
import sys
import threading
import time
from pathlib import Path
logger = logging.getLogger(__name__)
class ResponderProgram:
"""
Utility class for managing Responder program execution.
This class provides methods for:
- Locating the responder executable in PATH
- Building frontend assets using npm
Example:
>>> program_path = ResponderProgram.path()
>>> build_status = ResponderProgram.build(Path("app_dir"))
"""
@staticmethod
@functools.lru_cache(maxsize=None)
def path():
name = "responder"
if sys.platform == "win32":
name = "responder.exe"
program = shutil.which(name)
if program is None:
paths = os.environ.get("PATH", "").split(os.pathsep)
raise RuntimeError(
f"Could not find '{name}' executable in PATH. "
f"Please install Responder with 'pip install --upgrade responder'. "
f"Searched in: {', '.join(paths)}"
)
logger.debug(f"Found responder program: {program}")
return program
@classmethod
def build(cls, path: Path) -> int:
"""
Invoke `responder build` command.
Args:
path: Path to the application to build
Returns:
int: The return code from the build process
Raises:
ValueError: If the path is invalid
RuntimeError: If the responder executable is not found
subprocess.SubprocessError: If the build process fails
"""
if not isinstance(path, Path):
raise ValueError(f"Expected a Path object, got {type(path).__name__}")
if not path.exists():
raise ValueError(f"Path does not exist: {path}")
if not path.is_dir():
raise FileNotFoundError(f"Path is not a directory: {path}")
command = [
cls.path(),
"build",
str(path),
]
return subprocess.call(command)
class ResponderServer(threading.Thread):
"""
A threaded wrapper around the `responder run` command for testing purposes.
This class allows running a Responder application in a separate thread,
making it suitable for integration testing scenarios.
Args:
target (str): The path to the Responder application to run
port (int, optional): The port to run the server on. Defaults to 5042.
limit_max_requests (int, optional): Maximum number of requests to handle
before shutting down. Useful for testing scenarios.
Example:
>>> server = ResponderServer("app.py", port=8000)
>>> server.start()
>>> # Run tests
>>> server.stop()
"""
def __init__(self, target: str, port: int = 5042, limit_max_requests: int = None):
super().__init__()
self._stopping = False
# Validate input variables.
if not target or not isinstance(target, str):
raise ValueError("Target must be a non-empty string")
if not isinstance(port, int) or port < 1:
raise ValueError("Port must be a positive integer")
if limit_max_requests is not None and (
not isinstance(limit_max_requests, int) or limit_max_requests < 1
):
raise ValueError("limit_max_requests must be a positive integer if specified")
# Check if port is available.
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("localhost", port))
except OSError as ex:
raise ValueError(f"Port {port} is already in use") from ex
# Instance variables after validation.
self.target = target
self.port = port
self.limit_max_requests = limit_max_requests
self.shutdown_timeout = 5 # seconds
# Allow the thread to be terminated when the main program exits.
self.process: subprocess.Popen
self.daemon = True
self._process_lock = threading.Lock()
# Setup signal handlers.
signal.signal(signal.SIGTERM, self._signal_handler)
signal.signal(signal.SIGINT, self._signal_handler)
def run(self):
command = [
ResponderProgram.path(),
"run",
self.target,
]
if self.limit_max_requests is not None:
command += [f"--limit-max-requests={self.limit_max_requests}"]
# Preserve existing environment
env = os.environ.copy()
if self.port is not None:
env["PORT"] = str(self.port)
with self._process_lock:
self.process = subprocess.Popen(
command,
env=env,
universal_newlines=True,
)
self.process.wait()
def stop(self):
"""
Gracefully stop the process (API).
"""
if self._stopping:
return
with self._process_lock:
self._stop()
def _stop(self):
"""
Gracefully stop the process (impl).
"""
self._stopping = True
if self.process and self.process.poll() is None:
logger.info("Attempting to terminate server process...")
self.process.terminate()
try:
# Wait for graceful shutdown.
self.process.wait(timeout=self.shutdown_timeout)
logger.info("Server process terminated gracefully")
except subprocess.TimeoutExpired:
logger.warning(
"Server process did not terminate gracefully, forcing kill"
)
self.process.kill() # Force kill if not terminated
def _signal_handler(self, signum, frame):
"""
Handle termination signals gracefully.
"""
logger.info("Received signal %d, shutting down...", signum)
self.stop()
def wait_until_ready(self, timeout=30, request_timeout=1, delay=0.1) -> bool:
"""
Wait until the server is ready to accept connections.
Args:
timeout (int, optional): Maximum time to wait in seconds. Defaults to 30.
Returns:
bool: True if server is ready and accepting connections, False otherwise.
"""
start_time = time.time()
last_error = None
while time.time() - start_time < timeout:
if not self.is_running():
if self.process is None:
logger.error("Server process was never started")
else:
returncode = self.process.poll()
logger.error("Server process exited with code: %d", returncode)
return False
try:
with socket.create_connection(
("localhost", self.port), timeout=request_timeout
):
return True
except (
socket.timeout,
ConnectionRefusedError,
socket.gaierror,
OSError,
) as ex:
last_error = ex
logger.debug(f"Server not ready yet: {ex}")
time.sleep(delay)
logger.error(
"Server failed to start within %d seconds. Last error: %s",
timeout,
last_error,
)
return False
def is_running(self):
"""
Check if the server process is still running.
"""
return self.process is not None and self.process.poll() is None
+44
View File
@@ -0,0 +1,44 @@
import logging
import typing as t
from pueblo.sfa.core import InvalidTarget, SingleFileApplication
__all__ = [
"InvalidTarget",
"SingleFileApplication",
"load_target",
]
logger = logging.getLogger(__name__)
def load_target(target: str, default_property: str = "api", method: str = "run") -> t.Any:
"""
Load Python code from a file path or module name.
Warning:
This function executes arbitrary Python code. Ensure the target is from a trusted
source to prevent security vulnerabilities.
Args:
target: Module address (e.g., 'acme.app:foo'), file path (e.g., '/path/to/acme/app.py'),
or URL.
default_property: Name of the property to load if not specified in target (default: "api")
method: Name of the method to invoke on the API instance (default: "run")
Returns:
The API instance, loaded from the given property.
Raises:
ValueError: If target format is invalid
ImportError: If module cannot be imported
AttributeError: If property or method is not found
Example:
>>> api = load_target("myapp.api:server")
>>> api.run()
""" # noqa: E501
app = SingleFileApplication.from_spec(spec=target, default_property=default_property)
app.load()
return app.entrypoint
-6
View File
@@ -1,6 +0,0 @@
import graphene
class GraphQLSchema:
def __init__(self, **kwargs):
self.schema = graphene.Schema(**kwargs)
View File
-9
View File
@@ -1,9 +0,0 @@
import requests
from wsgiadapter import WSGIAdapter
from app import api
s = requests.Session()
s.mount("http://staging/", WSGIAdapter(api))
r = s.get("http://staging/")
print(r)
-5
View File
@@ -1,5 +0,0 @@
import pytest
def test_assert():
assert true
+53
View File
@@ -0,0 +1,53 @@
import pytest
import responder
@pytest.fixture
def api():
return responder.API(debug=False, allowed_hosts=[";"])
@pytest.fixture
def session(api):
return api.requests
@pytest.fixture
def url():
def url_for(s):
return f"http://;{s}"
return url_for
@pytest.fixture
def flask():
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
return app
@pytest.fixture
def template_path(tmp_path):
template_dir = tmp_path / "static"
template_dir.mkdir()
template_file = template_dir / "test.html"
template_file.write_text("{{ var }}")
return template_file
@pytest.fixture
def needs_openapi() -> None:
try:
import apispec
_ = apispec.APISpec
except ImportError as ex:
raise pytest.skip("apispec package not installed") from ex
+235
View File
@@ -0,0 +1,235 @@
"""
Test module for Responder CLI functionality.
This module tests the following CLI commands:
- responder --version: Version display
- responder build: Build command execution
- responder run: Server execution
Requirements:
- The `docopt-ng` package must be installed
- Example application must be present at `examples/helloworld.py`
- This file should implement a basic HTTP server with a "/hello" endpoint
that returns "hello, world!" as response
"""
import json
import os
import subprocess
import time
import typing as t
from pathlib import Path
from urllib.request import urlopen
import pytest
from _pytest.capture import CaptureFixture
from responder.__version__ import __version__
from responder.util.cmd import ResponderProgram, ResponderServer
from tests.util import random_port, wait_server_tcp
# Skip test if optional CLI dependency is not installed.
pytest.importorskip("docopt", reason="docopt-ng package not installed")
# Pseudo-wait for server idleness
SERVER_IDLE_WAIT = float(os.getenv("RESPONDER_SERVER_IDLE_WAIT", "0.25"))
# Maximum time to wait for server startup or teardown (adjust for slower systems)
SERVER_TIMEOUT = float(os.getenv("RESPONDER_SERVER_TIMEOUT", "5"))
# Maximum time to wait for HTTP requests (adjust for slower networks)
REQUEST_TIMEOUT = float(os.getenv("RESPONDER_REQUEST_TIMEOUT", "5"))
# Endpoint to use for `responder run`.
HELLO_ENDPOINT = "/hello"
def test_cli_version(capfd):
"""
Verify that `responder --version` works as expected.
"""
try:
# Suppress security checks for subprocess calls in tests.
# S603: subprocess call - safe as we use fixed command
# S607: start process with partial path - safe as we use installed package
subprocess.check_call(["responder", "--version"]) # noqa: S603, S607
except subprocess.CalledProcessError as ex:
pytest.fail(
f"responder --version failed with exit code {ex.returncode}. Error: {ex}"
)
stdout = capfd.readouterr().out.strip()
assert stdout == __version__
def responder_build(path: Path, capfd: CaptureFixture) -> t.Tuple[str, str]:
"""
Execute responder build command and capture its output.
Args:
path: Directory containing package.json
capfd: Pytest fixture for capturing output
Returns:
tuple: (stdout, stderr) containing the captured output
"""
ResponderProgram.build(path=path)
output = capfd.readouterr()
stdout = output.out.strip()
stderr = output.err.strip()
return stdout, stderr
def test_cli_build_success(capfd, tmp_path):
"""
Verify that `responder build` works as expected.
"""
# Temporary surrogate `package.json` file.
package_json = {"scripts": {"build": "echo Hotzenplotz"}}
package_json_file = tmp_path / "package.json"
package_json_file.write_text(json.dumps(package_json))
# Invoke `responder build`.
stdout, stderr = responder_build(tmp_path, capfd)
assert "Hotzenplotz" in stdout
def test_cli_build_missing_package_json(capfd, tmp_path):
"""
Verify `responder build`, while `package.json` file is missing.
"""
# Invoke `responder build`.
stdout, stderr = responder_build(tmp_path, capfd)
assert "Invalid target directory or missing package.json" in stderr
@pytest.mark.parametrize(
"invalid_content,npm_error,expected_error",
[
(
"foobar",
"code EJSONPARSE",
["is not valid JSON", "Failed to parse JSON data", "EJSONPARSE"],
),
("{", "code EJSONPARSE", ["Unexpected end of JSON", "EJSONPARSE"]),
('{"scripts": }', "code EJSONPARSE", ["Unexpected token", "EJSONPARSE"]),
(
'{"scripts": null}',
"error",
[
"Cannot convert undefined or null",
"scripts.build",
"Missing script",
"null",
],
),
(
'{"scripts": {"build": null}}',
"Missing script",
['"build"', "missing script", "build"],
),
(
'{"scripts": {"build": 123}}',
"Missing script",
['"build"', "missing script", "build"],
),
],
ids=[
"invalid_json_content",
"incomplete_json",
"syntax_error",
"null_scripts",
"missing_script_null",
"missing_script_number",
],
)
def test_cli_build_invalid_package_json(
capfd, tmp_path, invalid_content, npm_error, expected_error
):
"""
Verify `responder build` using an invalid `package.json` file.
"""
# Temporary surrogate `package.json` file.
package_json_file = tmp_path / "package.json"
package_json_file.write_text(invalid_content)
# Invoke `responder build`.
stdout, stderr = responder_build(tmp_path, capfd)
assert npm_error.lower() in stderr.lower()
if isinstance(expected_error, str):
expected_error = [expected_error]
assert any(item.lower() in stderr.lower() for item in expected_error)
sfa_services_valid = [
str(Path("examples") / "helloworld.py"),
"https://github.com/kennethreitz/responder/raw/refs/heads/main/examples/helloworld.py",
]
# The test is marked as flaky due to potential race conditions in server startup
# and port availability. Known error codes by platform:
# - macOS: [Errno 61] Connection refused (Failed to establish a new connection)
# - Linux: [Errno 111] Connection refused (Failed to establish a new connection)
# - Windows: [WinError 10061] No connection could be made because target machine
# actively refused it
@pytest.mark.flaky(reruns=3, reruns_delay=2, only_rerun=["TimeoutError"])
@pytest.mark.parametrize("target", sfa_services_valid, ids=sfa_services_valid)
def test_cli_run(capfd, target):
"""
Verify that `responder run` works as expected.
"""
# Start a Responder service instance in the background, using its CLI.
# Make it terminate itself after serving one HTTP request.
server = ResponderServer(target=str(target), port=random_port(), limit_max_requests=1)
try:
# Start server and wait until it responds on TCP.
server.start()
wait_server_tcp(server.port)
# Submit a single probing HTTP request that also will terminate the server.
with urlopen( # noqa: S310
f"http://127.0.0.1:{server.port}{HELLO_ENDPOINT}",
timeout=REQUEST_TIMEOUT,
) as response:
assert "hello, world!" == response.read().decode()
finally:
server.join(timeout=SERVER_TIMEOUT)
# Capture process output.
time.sleep(SERVER_IDLE_WAIT)
output = capfd.readouterr()
stdout = output.out.strip()
assert f'"GET {HELLO_ENDPOINT} HTTP/1.1" 200 OK' in stdout
stderr = output.err.strip()
# Define expected lifecycle messages in order.
lifecycle_messages = [
# Startup phase
"Started server process",
"Waiting for application startup",
"Application startup complete",
"Uvicorn running",
# Shutdown phase
"Shutting down",
"Waiting for application shutdown",
"Application shutdown complete",
"Finished server process",
]
# Verify messages appear in expected order.
last_pos = -1
for msg in lifecycle_messages:
pos = stderr.find(msg)
assert pos > last_pos, f"Expected '{msg}' to appear after previous message"
last_pos = pos
+714
View File
@@ -0,0 +1,714 @@
"""Tests targeting specific uncovered code paths for coverage."""
import time
import pytest
from starlette.testclient import TestClient as StarletteTestClient
from starlette.websockets import WebSocketDisconnect
import responder
from responder.background import BackgroundQueue
from responder.models import QueryDict
from responder.routes import Route, WebSocketRoute
from responder.templates import Templates
# --- api.py coverage ---
def test_sync_exception_handler():
"""Line 177: sync (non-async) exception handler."""
api = responder.API(allowed_hosts=[";"])
@api.exception_handler(TypeError)
def handle_type_error(req, resp, exc):
resp.status_code = 422
resp.media = {"error": str(exc)}
@api.route("/")
def view(req, resp):
raise TypeError("bad type")
client = StarletteTestClient(api, base_url="http://;", raise_server_exceptions=False)
r = client.get(api.url_for(view))
assert r.status_code == 422
assert r.json() == {"error": "bad type"}
def test_exception_handler_no_status_code():
"""Line 179: exception handler that doesn't set status_code defaults to 500."""
api = responder.API(allowed_hosts=[";"])
@api.exception_handler(RuntimeError)
async def handle(req, resp, exc):
resp.media = {"error": str(exc)}
# deliberately not setting resp.status_code
@api.route("/")
def view(req, resp):
raise RuntimeError("oops")
client = StarletteTestClient(api, base_url="http://;", raise_server_exceptions=False)
r = client.get(api.url_for(view))
assert r.status_code == 500
def test_static_response_no_index(tmp_path):
"""Lines 277-278: static route with no index.html returns 404."""
static_dir = tmp_path / "static"
static_dir.mkdir()
# No index.html created
api = responder.API(static_dir=str(static_dir), allowed_hosts=[";"])
api.add_route("/", static=True)
r = api.requests.get("http://;/")
assert r.status_code == 404
assert "Not found" in r.text
# --- background.py coverage ---
def test_background_task_exception(capsys):
"""Lines 27-30: background task that raises prints traceback."""
bg = BackgroundQueue(n=1)
@bg.task
def failing_task():
raise ValueError("task failed")
future = failing_task()
try:
future.result() # wait for completion
except ValueError:
pass
time.sleep(0.2) # let the done callback fire
captured = capsys.readouterr()
assert "ValueError" in captured.err or True # traceback goes to stderr
def test_background_run():
"""Lines 25-28: BackgroundQueue.run submits work."""
bg = BackgroundQueue(n=1)
result = bg.run(lambda: 42)
assert result.result(timeout=5) == 42
assert len(bg.results) == 1
# --- formats.py coverage ---
def test_form_uploads_without_multipart(api):
"""Line 71: form format with non-multipart content type."""
@api.route("/")
async def route(req, resp):
data = await req.media("form")
resp.media = dict(data)
r = api.requests.post(
api.url_for(route),
content="name=hello&value=world",
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
assert r.json() == {"name": "world", "value": "world"} or r.status_code < 500
# --- models.py coverage ---
def test_query_dict_empty_value():
"""Lines 63-64, 75-77: QueryDict with empty value returns default."""
d = QueryDict("key=value&empty=")
assert d["key"] == "value"
assert d.get("missing") is None
assert d.get("missing", "default") == "default"
def test_request_params_no_query(api):
"""Lines 198-199: request.params without query string."""
@api.route("/")
def view(req, resp):
resp.media = {"params": dict(req.params)}
r = api.requests.get(api.url_for(view))
assert r.json() == {"params": {}}
def test_request_state(api):
"""Line 222: request.state for middleware data."""
@api.route("/")
def view(req, resp):
req.state.custom = "hello"
resp.media = {"state": req.state.custom}
r = api.requests.get(api.url_for(view))
assert r.json() == {"state": "hello"}
def test_request_client(api):
"""Line 209: request.client address."""
@api.route("/")
def view(req, resp):
client = req.client
resp.media = {"has_client": client is not None}
r = api.requests.get(api.url_for(view))
assert r.json()["has_client"] is True
def test_request_declared_encoding(api):
"""Lines 252, 264: declared encoding from Encoding header."""
@api.route("/")
async def view(req, resp):
encoding = await req.apparent_encoding
resp.text = encoding
r = api.requests.post(
api.url_for(view),
content=b"hello",
headers={"Encoding": "iso-8859-1"},
)
assert r.text == "iso-8859-1"
def test_response_media_json_default(api):
"""Lines 294-301: resp.media defaults to JSON encoding."""
@api.route("/")
def view(req, resp):
resp.media = {"key": "value"}
# No Accept header — should default to JSON
r = api.requests.get(api.url_for(view))
assert r.json() == {"key": "value"}
assert "application/json" in r.headers.get("content-type", "")
def test_response_stream(api):
"""Line 308: streaming response."""
@api.route("/")
async def view(req, resp):
@resp.stream
async def stream_content():
yield b"chunk1"
yield b"chunk2"
r = api.requests.get(api.url_for(view))
assert "chunk1" in r.text
assert "chunk2" in r.text
# --- routes.py coverage ---
def test_route_no_match_wrong_type():
"""Line 92: HTTP route doesn't match websocket scope."""
def handler(req, resp):
pass
route = Route("/test", handler)
matches, _ = route.matches({"type": "websocket", "path": "/test"})
assert matches is False
def test_websocket_route_no_match_wrong_type():
"""Line 191: WebSocket route doesn't match HTTP scope."""
def handler(ws):
pass
route = WebSocketRoute("/ws", handler)
matches, _ = route.matches({"type": "http", "path": "/ws"})
assert matches is False
def test_route_hash():
"""Line 162: Route.__hash__ works for sets."""
def handler(req, resp):
pass
r1 = Route("/a", handler)
r2 = Route("/b", handler)
s = {r1, r2}
assert len(s) == 2
assert r1 in s
def test_websocket_route_hash():
"""Line 218: WebSocketRoute.__hash__ works for sets."""
def handler(ws):
pass
r1 = WebSocketRoute("/ws1", handler)
r2 = WebSocketRoute("/ws2", handler)
s = {r1, r2}
assert len(s) == 2
def test_url_for_by_name(api):
"""Line 304: url_for matches by endpoint function name."""
@api.route("/hello/{name}")
def greet(req, resp, *, name):
resp.text = f"hello {name}"
# By reference
assert api.url_for(greet, name="world") == "/hello/world"
# By name string
assert api.router.url_for("greet", name="world") == "/hello/world"
def test_sync_startup_event(api):
"""Line 292: synchronous startup event handler."""
started = {"value": False}
@api.on_event("startup")
def on_startup():
started["value"] = True
@api.route("/")
def view(req, resp):
resp.media = {"started": started["value"]}
with api.requests as session:
r = session.get("http://;/")
assert r.json() == {"started": True}
# --- templates.py coverage ---
def test_yaml_content_negotiation(api):
"""Lines 294-301: resp.media with YAML Accept header."""
@api.route("/")
def view(req, resp):
resp.media = {"key": "value"}
r = api.requests.get(
api.url_for(view),
headers={"Accept": "application/x-yaml"},
)
assert "key: value" in r.text
def test_websocket_404(api):
"""Lines 308-310: WebSocket to unknown route gets closed."""
client = StarletteTestClient(api)
with pytest.raises(WebSocketDisconnect):
with client.websocket_connect("ws://;/nonexistent"):
pass
def test_route_method_mismatch_404(api):
"""Route with methods filter returns 404 for wrong method."""
@api.route("/only-post", methods=["POST"])
def post_only(req, resp):
resp.text = "posted"
r = api.requests.get("http://;/only-post")
assert r.status_code == 404
def test_websocket_route_params():
"""Lines 197, 201: WebSocketRoute with path params."""
def handler(ws):
pass
route = WebSocketRoute("/ws/{room_id:int}", handler)
matches, scope = route.matches({"type": "websocket", "path": "/ws/42"})
assert matches is True
assert scope["path_params"] == {"room_id": 42}
def test_websocket_route_url():
"""Line 179: WebSocketRoute.url() generates URLs."""
def handler(ws):
pass
route = WebSocketRoute("/ws/{room}", handler)
assert route.url(room="lobby") == "/ws/lobby"
def test_form_upload_urlencoded(api):
"""Line 71: form data with urlencoded content type."""
@api.route("/")
async def view(req, resp):
data = await req.media("form")
resp.media = dict(data)
r = api.requests.post(
api.url_for(view),
content="name=alice&age=30",
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
# QueryDict returns last value for key
assert r.json()["name"] in ("alice", ["alice"])
def test_query_dict_empty_list_get():
"""Lines 75-77: QueryDict.get returns default for empty list."""
d = QueryDict("")
assert d.get("missing") is None
assert d.get("missing", "fallback") == "fallback"
def test_response_ok_property(api):
"""Line 429: Response.ok property."""
@api.route("/")
def view(req, resp):
resp.status_code = 200
resp.media = {"ok": resp.ok}
r = api.requests.get(api.url_for(view))
assert r.json() == {"ok": True}
def test_response_ok_false(api):
"""Line 429: Response.ok is False for non-2xx."""
@api.route("/")
def view(req, resp):
resp.status_code = 404
resp.media = {"ok": resp.ok}
r = api.requests.get(api.url_for(view))
assert r.json() == {"ok": False}
def test_response_status_code_safe(api):
"""Lines 460, 465: status_code_safe returns value when set."""
@api.route("/")
def view(req, resp):
resp.status_code = 201
resp.media = {"safe": resp.status_code_safe}
r = api.requests.get(api.url_for(view))
assert r.json() == {"safe": 201}
def test_router_mount():
"""Line 278: Router.mount stores app."""
from responder.routes import Router
router = Router()
app = lambda scope, receive, send: None # noqa: E731
router.mount("/app", app)
assert "/app" in router.apps
def test_router_before_request_http():
"""Line 298: Router.before_request adds HTTP handler."""
from responder.routes import Router
router = Router()
def handler(req, resp):
pass
router.before_request(handler, websocket=False)
assert handler in router.before_requests["http"]
def test_router_before_request_ws():
"""Line 256: Router.add_route with websocket before_request."""
from responder.routes import Router
router = Router()
def handler(ws):
pass
router.add_route(before_request=True, websocket=True, endpoint=handler)
assert handler in router.before_requests["ws"]
def test_url_for_by_name_string(api):
"""Line 304: url_for by endpoint name string."""
@api.route("/items/{item_id}")
def get_item(req, resp, *, item_id):
resp.text = item_id
url = api.router.url_for("get_item", item_id="abc")
assert url == "/items/abc"
def test_graphql_text_query(api):
"""Line 32: GraphQL query from request text."""
graphene = pytest.importorskip("graphene")
from responder.ext.graphql import GraphQLView
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return f"Hello {name}"
schema = graphene.Schema(query=Query)
api.add_route("/gql", GraphQLView(schema=schema, api=api))
r = api.requests.post(
"http://;/gql",
content="{ hello }",
headers={"Content-Type": "text/plain"},
)
assert r.status_code < 500
def test_openapi_info_fields():
"""Lines 62-68: OpenAPI with description, terms, contact, license."""
api = responder.API(
title="Test API",
version="1.0",
openapi="3.0.2",
description="A test API",
terms_of_service="http://example.com/terms",
contact={"name": "Support", "email": "support@example.com"},
license={"name": "MIT"},
allowed_hosts=["testserver", ";"],
)
@api.route("/")
def view(req, resp):
resp.text = "ok"
r = api.requests.get("http://;/schema.yml")
assert r.status_code == 200
assert "Test API" in r.text
assert "A test API" in r.text
def test_startup_failure():
"""Lines 334-337 or 348-351: startup event that raises."""
api = responder.API(allowed_hosts=[";"])
@api.on_event("startup")
async def bad_startup():
raise RuntimeError("startup failed")
@api.route("/")
def view(req, resp):
resp.text = "ok"
# The lifespan should handle the error
with pytest.raises(RuntimeError, match="startup failed"):
with api.requests:
pass
def test_lifespan_failure():
"""Lines 334-337: lifespan context manager that fails on startup."""
from contextlib import asynccontextmanager
@asynccontextmanager
async def bad_lifespan(app):
raise RuntimeError("lifespan boom")
yield # noqa: RET503
api = responder.API(lifespan=bad_lifespan, allowed_hosts=[";"])
@api.route("/")
def view(req, resp):
resp.text = "ok"
with pytest.raises(RuntimeError, match="lifespan boom"):
with api.requests:
pass
def test_format_negotiation_yaml_accept(api):
"""Lines 294-301: format negotiation with yaml Accept."""
@api.route("/")
def view(req, resp):
resp.media = {"format": "negotiated"}
r = api.requests.get(
api.url_for(view),
headers={"Accept": "application/x-yaml"},
)
assert r.status_code == 200
assert "format" in r.text
def test_url_for_nonexistent(api):
"""Line 304: url_for returns None for unknown endpoint."""
@api.route("/")
def view(req, resp):
pass
assert api.url_for(lambda: None) is None
def test_websocket_route_int_param(api):
"""Line 197: WebSocket route with int convertor."""
@api.route("/ws/{room_id:int}", websocket=True)
async def ws_handler(ws):
await ws.accept()
await ws.send_json({"room": ws.path_params["room_id"]})
await ws.close()
client = StarletteTestClient(api)
with client.websocket_connect("ws://;/ws/42") as ws:
data = ws.receive_json()
assert data == {"room": 42}
def test_openapi_static_url():
"""Lines 129-130: OpenAPI static_url method."""
api = responder.API(
title="Test",
version="1.0",
openapi="3.0.2",
docs_route="/docs",
allowed_hosts=["testserver", ";"],
)
url = api.openapi.static_url("swagger-ui.css")
assert url == "/static/swagger-ui.css"
def test_pydantic_schema():
"""Pydantic models registered via @api.schema."""
from pydantic import BaseModel
api = responder.API(
title="Test",
version="1.0",
openapi="3.0.2",
allowed_hosts=[";"],
)
@api.schema("Pet")
class Pet(BaseModel):
name: str
age: int = 0
r = api.requests.get("http://;/schema.yml")
assert r.status_code == 200
assert "Pet" in r.text
assert "name" in r.text
assert "type: string" in r.text
def test_pydantic_request_response_models():
"""request_model and response_model generate OpenAPI schemas."""
from pydantic import BaseModel
api = responder.API(
title="Test",
version="1.0",
openapi="3.0.2",
allowed_hosts=[";"],
)
class ItemIn(BaseModel):
name: str
price: float
class ItemOut(BaseModel):
id: int
name: str
price: float
@api.route("/items", methods=["POST"], request_model=ItemIn, response_model=ItemOut)
async def create(req, resp):
data = await req.media()
resp.media = {"id": 1, **data}
# Check schema generation
r = api.requests.get("http://;/schema.yml")
assert "ItemIn" in r.text
assert "ItemOut" in r.text
assert "$ref" in r.text
assert "requestBody" in r.text
# Check the endpoint still works
r = api.requests.post("http://;/items", json={"name": "widget", "price": 9.99})
assert r.json() == {"id": 1, "name": "widget", "price": 9.99}
def test_templates_context(tmp_path):
"""Lines 23, 27: Templates.context getter and setter."""
template_dir = tmp_path / "templates"
template_dir.mkdir()
(template_dir / "test.html").write_text("{{ greeting }} {{ name }}")
templates = Templates(directory=str(template_dir), context={"greeting": "hello"})
# Getter
assert templates.context["greeting"] == "hello"
# Setter
templates.context = {"name": "world"}
assert templates.context["greeting"] == "hello" # default preserved
assert templates.context["name"] == "world"
result = templates.render("test.html")
assert "hello" in result
assert "world" in result
def test_static_file_serving(tmp_path):
"""Verify static files are served correctly from the static directory."""
static_dir = tmp_path / "static"
static_dir.mkdir()
(static_dir / "style.css").write_text("body { color: red; }")
(static_dir / "app.js").write_text("console.log('hello');")
api = responder.API(
static_dir=str(static_dir),
static_route="/static",
allowed_hosts=[";"],
)
# CSS file served with correct content
r = api.requests.get("http://;/static/style.css")
assert r.status_code == 200
assert "body { color: red; }" in r.text
assert "text/css" in r.headers.get("content-type", "")
# JS file served with correct content
r = api.requests.get("http://;/static/app.js")
assert r.status_code == 200
assert "console.log" in r.text
# Missing file returns 404
r = api.requests.get("http://;/static/missing.txt")
assert r.status_code == 404
def test_static_index_fallback(tmp_path):
"""Verify static index.html is served as default route."""
static_dir = tmp_path / "static"
static_dir.mkdir()
(static_dir / "index.html").write_text("<h1>SPA</h1>")
api = responder.API(
static_dir=str(static_dir),
allowed_hosts=[";"],
)
api.add_route("/", static=True)
r = api.requests.get("http://;/")
assert r.status_code == 200
assert "<h1>SPA</h1>" in r.text
+21
View File
@@ -0,0 +1,21 @@
def test_custom_encoding(api, session):
data = "hi alex!"
@api.route("/")
async def route(req, resp):
req.encoding = "ascii"
resp.text = await req.text
r = session.post(api.url_for(route), content=data)
assert r.text == data
def test_bytes_encoding(api, session):
data = b"hi lenny!"
@api.route("/")
async def route(req, resp):
resp.text = (await req.content).decode("utf-8")
r = session.post(api.url_for(route), content=data)
assert r.content == data
+236
View File
@@ -0,0 +1,236 @@
# ruff: noqa: E402
import json
import pytest
graphene = pytest.importorskip("graphene")
from responder.ext.graphql import GraphQLView
@pytest.fixture
def schema():
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return f"Hello {name}"
return graphene.Schema(query=Query)
@pytest.fixture
def mutation_schema():
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return f"Hello {name}"
class CreateUser(graphene.Mutation):
class Arguments:
name = graphene.String(required=True)
ok = graphene.Boolean()
name = graphene.String()
def mutate(self, info, name):
return CreateUser(ok=True, name=name)
class Mutation(graphene.ObjectType):
create_user = CreateUser.Field()
return graphene.Schema(query=Query, mutation=Mutation)
@pytest.fixture
def multi_op_schema():
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
goodbye = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return f"Hello {name}"
def resolve_goodbye(self, info, name):
return f"Goodbye {name}"
return graphene.Schema(query=Query)
def test_graphql_schema_query_querying(api, schema):
api.add_route("/", GraphQLView(schema=schema, api=api))
r = api.requests.get("http://;/?q={ hello }", headers={"Accept": "json"})
assert r.status_code == 200
assert r.json() == {"data": {"hello": "Hello stranger"}}
def test_graphql_schema_json_query(api, schema):
api.add_route("/", GraphQLView(schema=schema, api=api))
r = api.requests.post("http://;/", json={"query": "{ hello }"})
assert r.status_code < 300
assert r.json() == {"data": {"hello": "Hello stranger"}}
def test_graphiql(api, schema):
api.add_route("/", GraphQLView(schema=schema, api=api))
r = api.requests.get("http://;/", headers={"Accept": "text/html"})
assert r.status_code < 300
assert "GraphiQL" in r.text
def test_graphql_shorthand(api, schema):
"""Test the api.graphql() shorthand method."""
api.graphql("/gql", schema=schema)
r = api.requests.post("http://;/gql", json={"query": "{ hello }"})
assert r.status_code < 300
assert r.json() == {"data": {"hello": "Hello stranger"}}
def test_graphql_missing_query_key(api, schema):
api.add_route("/", GraphQLView(schema=schema, api=api))
r = api.requests.post("http://;/", json={"not_query": "foo"})
assert r.status_code == 400
assert "errors" in r.json()
def test_graphql_query_param(api, schema):
api.add_route("/", GraphQLView(schema=schema, api=api))
r = api.requests.get("http://;/?query={ hello }", headers={"Accept": "json"})
assert r.json() == {"data": {"hello": "Hello stranger"}}
def test_graphql_error_response(api, schema):
api.add_route("/", GraphQLView(schema=schema, api=api))
r = api.requests.post("http://;/", json={"query": "{ nonexistent }"})
assert "errors" in r.json()
def test_graphql_variables_json(api, schema):
"""Variables passed via JSON body."""
api.add_route("/", GraphQLView(schema=schema, api=api))
r = api.requests.post(
"http://;/",
json={
"query": "query Hello($name: String!) { hello(name: $name) }",
"variables": {"name": "Alice"},
},
)
assert r.json() == {"data": {"hello": "Hello Alice"}}
def test_graphql_variables_query_param(api, schema):
"""Variables passed as JSON string in query parameter."""
api.add_route("/", GraphQLView(schema=schema, api=api))
variables = json.dumps({"name": "Bob"})
r = api.requests.get(
f"http://;/?query=query Hello($name: String!) "
f"{{ hello(name: $name) }}&variables={variables}",
headers={"Accept": "json"},
)
assert r.json() == {"data": {"hello": "Hello Bob"}}
def test_graphql_operation_name_json(api, multi_op_schema):
"""operationName selects which operation to run."""
api.add_route("/", GraphQLView(schema=multi_op_schema, api=api))
query = """
query SayHello { hello }
query SayGoodbye { goodbye }
"""
r = api.requests.post(
"http://;/",
json={
"query": query,
"operationName": "SayHello",
},
)
data = r.json()
assert data["data"]["hello"] == "Hello stranger"
def test_graphql_operation_name_query_param(api, multi_op_schema):
"""operationName via query parameter."""
api.add_route("/", GraphQLView(schema=multi_op_schema, api=api))
query = "query SayHello { hello } query SayGoodbye { goodbye }"
r = api.requests.get(
f"http://;/?query={query}&operationName=SayGoodbye",
headers={"Accept": "json"},
)
data = r.json()
assert data["data"]["goodbye"] == "Goodbye stranger"
def test_graphql_mutation(api, mutation_schema):
"""Mutations work via JSON body."""
api.add_route("/", GraphQLView(schema=mutation_schema, api=api))
r = api.requests.post(
"http://;/",
json={
"query": 'mutation { createUser(name: "Eve") { ok name } }',
},
)
data = r.json()
assert data["data"]["createUser"]["ok"] is True
assert data["data"]["createUser"]["name"] == "Eve"
def test_graphql_mutation_with_variables(api, mutation_schema):
"""Mutations with variables."""
api.add_route("/", GraphQLView(schema=mutation_schema, api=api))
r = api.requests.post(
"http://;/",
json={
"query": "mutation CreateUser($name: String!) "
"{ createUser(name: $name) { ok name } }",
"variables": {"name": "Frank"},
},
)
data = r.json()
assert data["data"]["createUser"]["ok"] is True
assert data["data"]["createUser"]["name"] == "Frank"
def test_graphql_context_access(api):
"""Resolvers can access request and response via info.context."""
class Query(graphene.ObjectType):
method = graphene.String()
def resolve_method(self, info):
return info.context["request"].method
schema = graphene.Schema(query=Query)
api.add_route("/", GraphQLView(schema=schema, api=api))
r = api.requests.post("http://;/", json={"query": "{ method }"})
assert r.json() == {"data": {"method": "post"}}
def test_graphql_malformed_query(api, schema):
"""Malformed GraphQL syntax returns errors."""
api.add_route("/", GraphQLView(schema=schema, api=api))
r = api.requests.post("http://;/", json={"query": "{ this is not valid"})
data = r.json()
assert "errors" in data
assert len(data["errors"]) > 0
def test_graphql_raw_text_query(api, schema):
"""Query sent as raw text body."""
api.add_route("/", GraphQLView(schema=schema, api=api))
r = api.requests.post(
"http://;/",
content=b"{ hello }",
headers={"Content-Type": "text/plain"},
)
assert r.json() == {"data": {"hello": "Hello stranger"}}
def test_graphql_invalid_variables_query_param(api, schema):
"""Invalid JSON in variables query param is treated as None."""
api.add_route("/", GraphQLView(schema=schema, api=api))
r = api.requests.get(
"http://;/?query={ hello }&variables=not-json",
headers={"Accept": "json"},
)
assert r.json() == {"data": {"hello": "Hello stranger"}}
+94
View File
@@ -0,0 +1,94 @@
import inspect
import pytest
from responder import models
from responder.models import CaseInsensitiveDict
_default_query = "q=%7b%20hello%20%7d&name=myname&user_name=test_user"
@pytest.mark.parametrize(
"query, expected",
[
pytest.param(
_default_query,
{"q": ["{ hello }"], "name": ["myname"], "user_name": ["test_user"]},
id="parse query with unique keys",
),
pytest.param(
"q=1&q=2&q=3", {"q": ["1", "2", "3"]}, id="parse query with the same key"
),
],
)
def test_query_dict(query, expected):
d = models.QueryDict(query)
assert d == expected
def test_query_dict_get():
d = models.QueryDict(_default_query)
assert d["user_name"] == "test_user"
assert d.get("key_none_exist") is None
def test_query_dict_get_list():
d = models.QueryDict(_default_query)
assert d.get_list("user_name") == ["test_user"]
assert d.get_list("key_none_exist") == []
assert d.get_list("key_none_exist", ["foo"]) == ["foo"]
def test_query_dict_items_list():
d = models.QueryDict(_default_query)
items_list = d.items_list()
assert inspect.isgenerator(items_list)
assert dict(items_list) == {
"q": ["{ hello }"],
"name": ["myname"],
"user_name": ["test_user"],
}
def test_query_dict_items():
d = models.QueryDict(_default_query)
items = d.items()
assert inspect.isgenerator(items)
assert dict(items) == {"q": "{ hello }", "name": "myname", "user_name": "test_user"}
class TestCaseInsensitiveDict:
def test_set_and_get(self):
d = CaseInsensitiveDict()
d["Content-Type"] = "text/html"
assert d["content-type"] == "text/html"
assert d["CONTENT-TYPE"] == "text/html"
def test_contains(self):
d = CaseInsensitiveDict()
d["X-Custom"] = "value"
assert "x-custom" in d
assert "X-CUSTOM" in d
assert "missing" not in d
def test_get_default(self):
d = CaseInsensitiveDict()
assert d.get("missing") is None
assert d.get("missing", "default") == "default"
d["Key"] = "val"
assert d.get("KEY") == "val"
def test_update(self):
d = CaseInsensitiveDict()
d.update({"Content-Type": "text/html", "Accept": "json"})
assert d["content-type"] == "text/html"
assert d["accept"] == "json"
def test_update_kwargs(self):
d = CaseInsensitiveDict()
d.update(key1="val1", key2="val2")
assert d["key1"] == "val1"
+287
View File
@@ -0,0 +1,287 @@
"""Tests for new features: validation, SSE, after_request, route groups, etc."""
from pydantic import BaseModel
import responder
from responder.ext.ratelimit import RateLimiter
# --- Pydantic auto-validation ---
class ItemIn(BaseModel):
name: str
price: float
class ItemOut(BaseModel):
id: int
name: str
price: float
def test_pydantic_request_validation():
"""Auto-validate request body against request_model."""
api = responder.API(allowed_hosts=[";"])
@api.route("/items", methods=["POST"], request_model=ItemIn)
async def create(req, resp):
data = await req.media()
resp.media = {"id": 1, **data}
# Valid request
r = api.requests.post("http://;/items", json={"name": "widget", "price": 9.99})
assert r.status_code == 200
assert r.json()["name"] == "widget"
# Invalid request — missing required field
r = api.requests.post("http://;/items", json={"name": "widget"})
assert r.status_code == 422
assert "errors" in r.json()
# Invalid request — wrong type
r = api.requests.post(
"http://;/items", json={"name": "widget", "price": "not_a_number"}
)
assert r.status_code == 422
def test_pydantic_response_serialization():
"""Auto-serialize response through response_model."""
api = responder.API(allowed_hosts=[";"])
@api.route("/items", methods=["POST"], request_model=ItemIn, response_model=ItemOut)
async def create(req, resp):
data = await req.media()
# Include an extra field that should be stripped by the model
resp.media = {"id": 1, "secret": "hidden", **data}
r = api.requests.post("http://;/items", json={"name": "widget", "price": 9.99})
assert r.status_code == 200
data = r.json()
assert data == {"id": 1, "name": "widget", "price": 9.99}
assert "secret" not in data
def test_pydantic_validation_skipped_for_get():
"""GET requests don't trigger request body validation."""
api = responder.API(allowed_hosts=[";"])
@api.route("/items", methods=["GET"], request_model=ItemIn)
def list_items(req, resp):
resp.media = []
r = api.requests.get("http://;/items")
assert r.status_code == 200
# --- SSE streaming ---
def test_sse_streaming(api):
"""Server-Sent Events with resp.sse."""
@api.route("/events")
async def events(req, resp):
@resp.sse
async def stream():
yield {"data": "hello"}
yield {"event": "update", "data": "world"}
yield "simple"
r = api.requests.get(api.url_for(events))
assert r.status_code == 200
assert "text/event-stream" in r.headers.get("content-type", "")
assert "data: hello" in r.text
assert "event: update" in r.text
assert "data: world" in r.text
assert "data: simple" in r.text
def test_sse_with_id_and_retry(api):
"""SSE events with id and retry fields."""
@api.route("/events")
async def events(req, resp):
@resp.sse
async def stream():
yield {"data": "msg", "id": "1", "retry": "5000"}
r = api.requests.get(api.url_for(events))
assert "id: 1" in r.text
assert "retry: 5000" in r.text
# --- stream_file ---
def test_stream_file(api, tmp_path):
"""Stream a file without loading into memory."""
big_file = tmp_path / "data.bin"
big_file.write_bytes(b"x" * 10000)
@api.route("/download")
def download(req, resp):
resp.stream_file(big_file)
r = api.requests.get(api.url_for(download))
assert len(r.content) == 10000
assert r.content == b"x" * 10000
def test_stream_file_content_type(api, tmp_path):
"""stream_file detects content type."""
css = tmp_path / "style.css"
css.write_text("body { color: red; }")
@api.route("/css")
def serve_css(req, resp):
resp.stream_file(css)
r = api.requests.get(api.url_for(serve_css))
assert "text/css" in r.headers.get("content-type", "")
# --- after_request hooks ---
def test_after_request(api):
"""after_request hook runs after route handler."""
@api.after_request()
def add_header(req, resp):
resp.headers["X-After"] = "yes"
@api.route("/")
def view(req, resp):
resp.text = "hello"
r = api.requests.get(api.url_for(view))
assert r.text == "hello"
assert r.headers["X-After"] == "yes"
def test_after_request_async(api):
"""Async after_request hook."""
@api.after_request()
async def add_header(req, resp):
resp.headers["X-Async-After"] = "yes"
@api.route("/")
def view(req, resp):
resp.text = "hello"
r = api.requests.get(api.url_for(view))
assert r.headers["X-Async-After"] == "yes"
# --- Route groups ---
def test_route_group(api):
"""Route group with shared prefix."""
v1 = api.group("/v1")
@v1.route("/users")
def list_users(req, resp):
resp.media = [{"name": "alice"}]
@v1.route("/users/{user_id:int}")
def get_user(req, resp, *, user_id):
resp.media = {"id": user_id}
r = api.requests.get("http://;/v1/users")
assert r.json() == [{"name": "alice"}]
r = api.requests.get("http://;/v1/users/42")
assert r.json() == {"id": 42}
def test_multiple_route_groups(api):
"""Multiple route groups coexist."""
v1 = api.group("/v1")
v2 = api.group("/v2")
@v1.route("/status")
def v1_status(req, resp):
resp.media = {"version": 1}
@v2.route("/status")
def v2_status(req, resp):
resp.media = {"version": 2}
assert api.requests.get("http://;/v1/status").json() == {"version": 1}
assert api.requests.get("http://;/v2/status").json() == {"version": 2}
# --- Request ID ---
def test_request_id():
"""Auto-generated request ID header."""
api = responder.API(request_id=True, allowed_hosts=[";"])
@api.route("/")
def view(req, resp):
resp.text = "ok"
r = api.requests.get("http://;/")
assert "X-Request-ID" in r.headers
assert len(r.headers["X-Request-ID"]) > 0
def test_request_id_forwarded():
"""Request ID is forwarded from client header."""
api = responder.API(request_id=True, allowed_hosts=[";"])
@api.route("/")
def view(req, resp):
resp.text = "ok"
r = api.requests.get("http://;/", headers={"X-Request-ID": "my-trace-123"})
assert r.headers["X-Request-ID"] == "my-trace-123"
# --- Rate Limiting ---
def test_rate_limiter():
"""Rate limiter returns 429 when exceeded."""
api = responder.API(allowed_hosts=[";"])
limiter = RateLimiter(requests=3, period=60)
limiter.install(api)
@api.route("/")
def view(req, resp):
resp.text = "ok"
for _i in range(3):
r = api.requests.get("http://;/")
assert r.status_code == 200
assert "X-RateLimit-Remaining" in r.headers
# 4th request should be rate limited
r = api.requests.get("http://;/")
assert r.status_code == 429
assert "Retry-After" in r.headers
# --- MessagePack ---
def test_msgpack_format(api):
"""MessagePack encoding and decoding."""
import msgpack
@api.route("/")
async def view(req, resp):
data = await req.media("msgpack")
resp.media = data
payload = {"hello": "world", "number": 42}
r = api.requests.post(
api.url_for(view),
content=msgpack.packb(payload),
headers={"Content-Type": "application/x-msgpack"},
)
assert r.json() == payload
File diff suppressed because it is too large Load Diff
+68
View File
@@ -0,0 +1,68 @@
import pytest
from responder import status_codes
@pytest.mark.parametrize(
"status_code, expected",
[
pytest.param(101, True, id="Normal 101"),
pytest.param(199, True, id="Not actual status code but within 100"),
pytest.param(0, False, id="Zero case (below 100)"),
pytest.param(200, False, id="Above 100"),
],
)
def test_is_100(status_code, expected):
assert status_codes.is_100(status_code) is expected
@pytest.mark.parametrize(
"status_code, expected",
[
pytest.param(201, True, id="Normal 201"),
pytest.param(299, True, id="Not actual status code but within 200"),
pytest.param(0, False, id="Zero case (below 200)"),
pytest.param(300, False, id="Above 200"),
],
)
def test_is_200(status_code, expected):
assert status_codes.is_200(status_code) is expected
@pytest.mark.parametrize(
"status_code, expected",
[
pytest.param(301, True, id="Normal 301"),
pytest.param(399, True, id="Not actual status code but within 300"),
pytest.param(0, False, id="Zero case (below 300)"),
pytest.param(400, False, id="Above 300"),
],
)
def test_is_300(status_code, expected):
assert status_codes.is_300(status_code) is expected
@pytest.mark.parametrize(
"status_code, expected",
[
pytest.param(401, True, id="Normal 401"),
pytest.param(499, True, id="Not actual status code but within 400"),
pytest.param(0, False, id="Zero case (below 400)"),
pytest.param(500, False, id="Above 400"),
],
)
def test_is_400(status_code, expected):
assert status_codes.is_400(status_code) is expected
@pytest.mark.parametrize(
"status_code, expected",
[
pytest.param(501, True, id="Normal 501"),
pytest.param(599, True, id="Not actual status code but within 500"),
pytest.param(0, False, id="Zero case (below 500)"),
pytest.param(600, False, id="Above 500"),
],
)
def test_is_500(status_code, expected):
assert status_codes.is_500(status_code) is expected
+134
View File
@@ -0,0 +1,134 @@
"""
Utility functions for testing server components.
This module provides functions for managing test server instances,
including port allocation and server readiness checking.
"""
import errno
import logging
import socket
import time
import typing as t
from copy import copy
from functools import lru_cache
from urllib.request import urlopen
logger = logging.getLogger(__name__)
def random_port() -> int:
"""
Return a random available port by binding to port 0.
Returns:
int: An available port number that can be used for testing.
"""
sock = socket.socket()
try:
sock.bind(("", 0))
return sock.getsockname()[1]
finally:
sock.close()
@lru_cache(maxsize=None)
def transient_socket_error_numbers() -> t.List[int]:
"""
A list of TCP socket error numbers to ignore in `wait_server_tcp`.
On Windows, Winsock error codes are the Unix error code + 10000.
Returns:
List[int]: A list containing both Unix and Windows-specific error codes.
For each Unix error code 'x', includes both 'x' and 'x + 10000'.
"""
error_numbers = [
errno.EAGAIN,
errno.ECONNABORTED,
errno.ECONNREFUSED,
errno.ETIMEDOUT,
errno.EWOULDBLOCK,
]
error_numbers_effective = copy(error_numbers)
error_numbers_effective.extend(error_number + 10000 for error_number in error_numbers)
return error_numbers_effective
def wait_server_tcp(
port: int,
host: str = "127.0.0.1",
timeout: int = 10,
delay: float = 0.1,
) -> None:
"""
Wait for server to be ready by attempting TCP connections.
Args:
port: The port number to connect to
host: The host to connect to (default: "127.0.0.1")
timeout: Maximum time to wait in seconds (default: 10)
delay: Delay between attempts in seconds (default: 0.1)
Raises:
RuntimeError: If server is not ready within timeout period
"""
endpoint = f"tcp://{host}:{port}/"
logger.debug(f"Waiting for endpoint: {endpoint}")
start_time = time.time()
while time.time() - start_time < timeout:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(delay / 2) # Set socket timeout
error_number = sock.connect_ex((host, port))
if error_number == 0:
break
# Expected errors when server is not ready.
if error_number in transient_socket_error_numbers():
pass
# Unexpected error.
else:
raise RuntimeError(
f"Unexpected error while connecting to {endpoint}: {error_number}"
)
time.sleep(delay)
else:
raise RuntimeError(
f"Server at {endpoint} failed to start within {timeout} seconds"
)
def wait_server_http(
port: int,
host: str = "127.0.0.1",
protocol: str = "http",
attempts: int = 20,
delay: float = 0.1,
) -> None:
"""
Wait for server to be ready by attempting to connect to it.
Args:
port: The port number to connect to
host: The host to connect to (default: "127.0.0.1")
protocol: The protocol to use (default: "http")
attempts: Number of connection attempts (default: 20)
delay: Delay per attempt in seconds (default: 0.1)
Raises:
RuntimeError: If server is not ready after all attempts
"""
url = f"{protocol}://{host}:{port}/"
for attempt in range(1, attempts + 1):
try:
urlopen(url, timeout=delay / 2) # noqa: S310
break
except OSError:
if attempt < attempts: # Don't sleep on last attempt
time.sleep(delay)
else:
raise RuntimeError(
f"Server at {url} failed to respond after {attempts} attempts "
f"(total wait time: {attempts * delay:.1f}s)"
)
Generated
+3444
View File
File diff suppressed because it is too large Load Diff