Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-starlette for 
openSUSE:Factory checked in at 2021-12-12 21:27:45
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-starlette (Old)
 and      /work/SRC/openSUSE:Factory/.python-starlette.new.2520 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-starlette"

Sun Dec 12 21:27:45 2021 rev:4 rq:940017 version:0.17.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-starlette/python-starlette.changes        
2021-09-27 20:09:26.782497669 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-starlette.new.2520/python-starlette.changes  
    2021-12-12 21:28:30.176373296 +0100
@@ -1,0 +2,25 @@
+Wed Dec  8 15:53:03 UTC 2021 - Torsten Gruner <[email protected]>
+
+- update to version 0.17.1
+  * Fix IndexError in authentication requires when wrapped function
+     arguments are distributed between *args and **kwargs #1335.
+- version 17.0
+  * Added
+    - Response.delete_cookie now accepts the same parameters as
+      Response.set_cookie #1228.
+    - Update the Jinja2Templates constructor to allow PathLike #1292.
+  * Fixed
+    - Fix BadSignature exception handling in SessionMiddleware #1264.
+    - Change HTTPConnection.__getitem__ return type from str to
+      typing.Any #1118.
+    - Change ImmutableMultiDict.getlist return type from typing.List[str]
+      to typing.List[typing.Any] #1235.
+    - Handle OSError exceptions on StaticFiles #1220.
+    - Fix StaticFiles 404.html in HTML mode #1314.
+    - Prevent anyio.ExceptionGroup in error views under a
+      BaseHTTPMiddleware #1262.
+  *Removed
+    - Remove GraphQL support #1198.
+- Remove py39-ignore-loop-deprecation.patch
+
+-------------------------------------------------------------------

Old:
----
  py39-ignore-loop-deprecation.patch
  starlette-0.16.0.tar.gz

New:
----
  starlette-0.17.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-starlette.spec ++++++
--- /var/tmp/diff_new_pack.8o1Les/_old  2021-12-12 21:28:30.600373547 +0100
+++ /var/tmp/diff_new_pack.8o1Les/_new  2021-12-12 21:28:30.600373547 +0100
@@ -19,16 +19,13 @@
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 %define skip_python2 1
 Name:           python-starlette
-Version:        0.16.0
+Version:        0.17.1
 Release:        0
 Summary:        Lightweight ASGI framework/toolkit
 License:        BSD-3-Clause
 Group:          Development/Languages/Python
 URL:            https://github.com/encode/starlette
 Source:         
https://github.com/encode/starlette/archive/refs/tags/%{version}.tar.gz#/starlette-%{version}.tar.gz
-# PATCH-FIX-UPSTREAM py39-ignore-loop-deprecation.patch 
gh#encode/starlette#1293 [email protected]
-# Ignore loop deprecation warnings originating inside asyncio; bpo#45097
-Patch0:         py39-ignore-loop-deprecation.patch
 BuildRequires:  %{python_module Jinja2}
 BuildRequires:  %{python_module PyYAML}
 BuildRequires:  %{python_module aiofiles}
@@ -58,7 +55,7 @@
 building high performance asyncio services.
 
 %prep
-%autosetup -p1 -n starlette-%{version}
+%autosetup -n starlette-%{version}
 
 %build
 %python_build
@@ -69,7 +66,7 @@
 
 %check
 # Deprecate built-in GraphQL support gh#encode/starlette#1135
-rm tests/test_graphql.py
+# rm tests/test_graphql.py
 # Remove unrecognized arguments: --strict-config --strict-markers
 sed -i "s|--strict-config||" setup.cfg
 sed -i "s|--strict-markers||" setup.cfg

++++++ starlette-0.16.0.tar.gz -> starlette-0.17.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/.github/workflows/test-suite.yml 
new/starlette-0.17.1/.github/workflows/test-suite.yml
--- old/starlette-0.16.0/.github/workflows/test-suite.yml       2021-07-19 
09:08:36.000000000 +0200
+++ new/starlette-0.17.1/.github/workflows/test-suite.yml       2021-11-17 
17:10:27.000000000 +0100
@@ -14,7 +14,7 @@
 
     strategy:
       matrix:
-        python-version: ["3.6", "3.7", "3.8", "3.9", "3.10.0-beta.3"]
+        python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
 
     steps:
       - uses: "actions/checkout@v2"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/README.md 
new/starlette-0.17.1/README.md
--- old/starlette-0.16.0/README.md      2021-07-19 09:08:36.000000000 +0200
+++ new/starlette-0.17.1/README.md      2021-11-17 17:10:27.000000000 +0100
@@ -28,7 +28,6 @@
 
 * Seriously impressive performance.
 * WebSocket support.
-* GraphQL support.
 * In-process background tasks.
 * Startup and shutdown events.
 * Test client built on `requests`.
@@ -92,7 +91,6 @@
 * [`python-multipart`][python-multipart] - Required if you want to support 
form parsing, with `request.form()`.
 * [`itsdangerous`][itsdangerous] - Required for `SessionMiddleware` support.
 * [`pyyaml`][pyyaml] - Required for `SchemaGenerator` support.
-* [`graphene`][graphene] - Required for `GraphQLApp` support.
 
 You can install all of these with `pip3 install starlette[full]`.
 
@@ -169,7 +167,6 @@
 [requests]: http://docs.python-requests.org/en/master/
 [jinja2]: http://jinja.pocoo.org/
 [python-multipart]: https://andrew-d.github.io/python-multipart/
-[graphene]: https://graphene-python.org/
 [itsdangerous]: https://pythonhosted.org/itsdangerous/
 [sqlalchemy]: https://www.sqlalchemy.org
 [pyyaml]: https://pyyaml.org/wiki/PyYAMLDocumentation
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/docs/config.md 
new/starlette-0.17.1/docs/config.md
--- old/starlette-0.16.0/docs/config.md 2021-07-19 09:08:36.000000000 +0200
+++ new/starlette-0.17.1/docs/config.md 2021-11-17 17:10:27.000000000 +0100
@@ -160,7 +160,7 @@
 ```python
 from starlette.applications import Starlette
 from starlette.middleware import Middleware
-from starlette.middleware.session import SessionMiddleware
+from starlette.middleware.sessions import SessionMiddleware
 from starlette.routing import Route
 from myproject import settings
 
@@ -192,7 +192,7 @@
 from starlette.config import environ
 from starlette.testclient import TestClient
 from sqlalchemy import create_engine
-from sqlalchemy_utils import database_exists, create_database
+from sqlalchemy_utils import create_database, database_exists, drop_database
 
 # This line would raise an error if we use it after 'settings' has been 
imported.
 environ['TESTING'] = 'TRUE'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/docs/database.md 
new/starlette-0.17.1/docs/database.md
--- old/starlette-0.16.0/docs/database.md       2021-07-19 09:08:36.000000000 
+0200
+++ new/starlette-0.17.1/docs/database.md       2021-11-17 17:10:27.000000000 
+0100
@@ -142,10 +142,10 @@
         await database.execute(query)
         raise RuntimeError()
     except:
-        transaction.rollback()
+        await transaction.rollback()
         raise
     else:
-        transaction.commit()
+        await transaction.commit()
 ```
 
 ## Test isolation
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/docs/graphql.md 
new/starlette-0.17.1/docs/graphql.md
--- old/starlette-0.16.0/docs/graphql.md        2021-07-19 09:08:36.000000000 
+0200
+++ new/starlette-0.17.1/docs/graphql.md        2021-11-17 17:10:27.000000000 
+0100
@@ -1,116 +1,9 @@
+GraphQL support in Starlette was deprecated in version 0.15.0, and removed in 
version 0.17.0.
 
-!!! Warning
+Although GraphQL support is no longer built in to Starlette, you can still use 
GraphQL with Starlette via 3rd party libraries. These libraries all have 
Starlette-specific guides to help you do just that:
 
-    GraphQL support in Starlette is **deprecated** as of version 0.15 and will
-    be removed in a future release. It is also incompatible with Python 3.10+.
-    Please consider using a third-party library to provide GraphQL support. 
This
-    is usually done by mounting a GraphQL ASGI application.
-    See [#619](https://github.com/encode/starlette/issues/619).
-    Some example libraries are:
+- [Ariadne](https://ariadnegraphql.org/docs/starlette-integration.html)
+- 
[`starlette-graphene3`](https://github.com/ciscorn/starlette-graphene3#example)
+- [Strawberry](https://strawberry.rocks/docs/integrations/starlette)
+- 
[`tartiflette-asgi`](https://tartiflette.github.io/tartiflette-asgi/usage/#starlette)
 
-    * [Ariadne](https://ariadnegraphql.org/docs/asgi)
-    * [`tartiflette-asgi`](https://tartiflette.github.io/tartiflette-asgi/)
-    * [Strawberry](https://strawberry.rocks/docs/integrations/asgi)
-    * [`starlette-graphene3`](https://github.com/ciscorn/starlette-graphene3)
-
-Starlette includes optional support for GraphQL, using the `graphene` library.
-
-Here's an example of integrating the support into your application.
-
-```python
-from starlette.applications import Starlette
-from starlette.routing import Route
-from starlette.graphql import GraphQLApp
-import graphene
-
-
-class Query(graphene.ObjectType):
-    hello = graphene.String(name=graphene.String(default_value="stranger"))
-
-    def resolve_hello(self, info, name):
-        return "Hello " + name
-
-routes = [
-    Route('/', GraphQLApp(schema=graphene.Schema(query=Query)))
-]
-
-app = Starlette(routes=routes)
-```
-
-If you load up the page in a browser, you'll be served the GraphiQL tool,
-which you can use to interact with your GraphQL API.
-
-
-![GraphiQL](img/graphiql.png)
-
-## Accessing request information
-
-The current request is available in the context.
-
-```python
-class Query(graphene.ObjectType):
-    user_agent = graphene.String()
-
-    def resolve_user_agent(self, info):
-        """
-        Return the User-Agent of the incoming request.
-        """
-        request = info.context["request"]
-        return request.headers.get("User-Agent", "<unknown>")
-```
-
-## Adding background tasks
-
-You can add background tasks to run once the response has been sent.
-
-```python
-class Query(graphene.ObjectType):
-    user_agent = graphene.String()
-
-    def resolve_user_agent(self, info):
-        """
-        Return the User-Agent of the incoming request.
-        """
-        user_agent = request.headers.get("User-Agent", "<unknown>")
-        background = info.context["background"]
-        background.add_task(log_user_agent, user_agent=user_agent)
-        return user_agent
-
-async def log_user_agent(user_agent):
-    ...
-```
-
-## Sync or Async executors
-
-If you're working with a standard ORM, then just use regular function calls for
-your "resolve" methods, and Starlette will manage running the GraphQL query 
within a
-separate thread.
-
-If you want to use an asynchronous ORM, then use "async resolve" methods, and
-make sure to setup Graphene's AsyncioExecutor using the `executor` argument.
-
-```python
-from graphql.execution.executors.asyncio import AsyncioExecutor
-from starlette.applications import Starlette
-from starlette.graphql import GraphQLApp
-from starlette.routing import Route
-import graphene
-
-
-class Query(graphene.ObjectType):
-    hello = graphene.String(name=graphene.String(default_value="stranger"))
-
-    async def resolve_hello(self, info, name):
-        # We can make asynchronous network calls here.
-        return "Hello " + name
-
-routes = [
-    # We're using `executor_class=AsyncioExecutor` here.
-    Route('/', GraphQLApp(
-        schema=graphene.Schema(query=Query),
-        executor_class=AsyncioExecutor
-    ))
-]
-
-app = Starlette(routes=routes)
-```
Binary files old/starlette-0.16.0/docs/img/graphiql.png and 
new/starlette-0.17.1/docs/img/graphiql.png differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/docs/index.md 
new/starlette-0.17.1/docs/index.md
--- old/starlette-0.16.0/docs/index.md  2021-07-19 09:08:36.000000000 +0200
+++ new/starlette-0.17.1/docs/index.md  2021-11-17 17:10:27.000000000 +0100
@@ -162,7 +162,6 @@
 [requests]: http://docs.python-requests.org/en/master/
 [jinja2]: http://jinja.pocoo.org/
 [python-multipart]: https://andrew-d.github.io/python-multipart/
-[graphene]: https://graphene-python.org/
 [itsdangerous]: https://pythonhosted.org/itsdangerous/
 [sqlalchemy]: https://www.sqlalchemy.org
 [pyyaml]: https://pyyaml.org/wiki/PyYAMLDocumentation
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/docs/middleware.md 
new/starlette-0.17.1/docs/middleware.md
--- old/starlette-0.16.0/docs/middleware.md     2021-07-19 09:08:36.000000000 
+0200
+++ new/starlette-0.17.1/docs/middleware.md     2021-11-17 17:10:27.000000000 
+0100
@@ -183,6 +183,8 @@
 `async def dispatch(request, call_next)` method.
 
 ```python
+from starlette.middleware.base import BaseHTTPMiddleware
+
 class CustomHeaderMiddleware(BaseHTTPMiddleware):
     async def dispatch(self, request, call_next):
         response = await call_next(request)
@@ -256,7 +258,7 @@
 A middleware class to emit timing information (cpu and wall time) for each 
request which
 passes through it.  Includes examples for how to emit these timings as statsd 
metrics.
 
-#### [datasette-auth-github](https://github.com/simonw/datasette-auth-github)
+#### [asgi-auth-github](https://github.com/simonw/asgi-auth-github)
 
 This middleware adds authentication to any ASGI application, requiring users 
to sign in
 using their GitHub account (via 
[OAuth](https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/)).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/docs/overrides/partials/nav.html 
new/starlette-0.17.1/docs/overrides/partials/nav.html
--- old/starlette-0.16.0/docs/overrides/partials/nav.html       1970-01-01 
01:00:00.000000000 +0100
+++ new/starlette-0.17.1/docs/overrides/partials/nav.html       2021-11-17 
17:10:27.000000000 +0100
@@ -0,0 +1,52 @@
+<!-- Determine class according to configuration -->
+ {% set class = "md-nav md-nav--primary" %}
+ {% if "navigation.tabs" in features %}
+   {% set class = class ~ " md-nav--lifted" %}
+ {% endif %}
+ {% if "toc.integrate" in features %}
+   {% set class = class ~ " md-nav--integrated" %}
+ {% endif %}
+
+ <!-- Main navigation -->
+ <nav
+   class="{{ class }}"
+   aria-label="{{ lang.t('nav.title') }}"
+   data-md-level="0"
+ >
+
+   <!-- Site title -->
+   <label class="md-nav__title" for="__drawer">
+     <a
+       href="{{ config.extra.homepage | d(nav.homepage.url, true) | url }}"
+       title="{{ config.site_name | e }}"
+       class="md-nav__button md-logo"
+       aria-label="{{ config.site_name }}"
+       data-md-component="logo"
+     >
+       {% include "partials/logo.html" %}
+     </a>
+     {{ config.site_name }}
+   </label>
+
+   <!-- Repository information -->
+   {% if config.repo_url %}
+     <div class="md-nav__source">
+       {% include "partials/source.html" %}
+     </div>
+   {% endif %}
+
+   <!-- Render item list -->
+   <ul class="md-nav__list" data-md-scrollfix>
+     {% for nav_item in nav %}
+       {% set path = "__nav_" ~ loop.index %}
+       {% set level = 1 %}
+       {% include "partials/nav-item.html" %}
+     {% endfor %}
+   </ul>
+
+   <ul class="md-nav__list" data-md-scrollfix style="padding-top: 15px; 
padding-left: 10px">
+     <div>
+       <a href="https://fastapi.tiangolo.com";><img src="/sponsors/fastapi.png" 
width=150px style=></img></a>
+     </div>
+   </ul>
+ </nav>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/docs/release-notes.md 
new/starlette-0.17.1/docs/release-notes.md
--- old/starlette-0.16.0/docs/release-notes.md  2021-07-19 09:08:36.000000000 
+0200
+++ new/starlette-0.17.1/docs/release-notes.md  2021-11-17 17:10:27.000000000 
+0100
@@ -1,3 +1,29 @@
+## 0.17.1
+
+November 17, 2021
+
+### Fixed
+* Fix `IndexError` in authentication `requires` when wrapped function 
arguments are distributed between `*args` and `**kwargs` 
[#1335](https://github.com/encode/starlette/pull/1335).
+
+## 0.17.0
+
+November 4, 2021
+
+### Added
+* `Response.delete_cookie` now accepts the same parameters as 
`Response.set_cookie` [#1228](https://github.com/encode/starlette/pull/1228).
+* Update the `Jinja2Templates` constructor to allow `PathLike` 
[#1292](https://github.com/encode/starlette/pull/1292).
+
+### Fixed
+* Fix BadSignature exception handling in SessionMiddleware 
[#1264](https://github.com/encode/starlette/pull/1264).
+* Change `HTTPConnection.__getitem__` return type from `str` to `typing.Any` 
[#1118](https://github.com/encode/starlette/pull/1118).
+* Change `ImmutableMultiDict.getlist` return type from `typing.List[str]` to 
`typing.List[typing.Any]` 
[#1235](https://github.com/encode/starlette/pull/1235).
+* Handle `OSError` exceptions on `StaticFiles` 
[#1220](https://github.com/encode/starlette/pull/1220).
+* Fix `StaticFiles` 404.html in HTML mode 
[#1314](https://github.com/encode/starlette/pull/1314).
+* Prevent anyio.ExceptionGroup in error views under a BaseHTTPMiddleware 
[#1262](https://github.com/encode/starlette/pull/1262).
+
+### Removed
+* Remove GraphQL support 
[#1198](https://github.com/encode/starlette/pull/1198).
+
 ## 0.16.0
 
 July 19, 2021
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/docs/responses.md 
new/starlette-0.17.1/docs/responses.md
--- old/starlette-0.16.0/docs/responses.md      2021-07-19 09:08:36.000000000 
+0200
+++ new/starlette-0.17.1/docs/responses.md      2021-11-17 17:10:27.000000000 
+0100
@@ -182,9 +182,8 @@
     await response(scope, receive, send)
 ```
 
-## Third party middleware
+## Third party responses
 
-### [SSEResponse(EventSourceResponse)](https://github.com/sysid/sse-starlette)
+#### [EventSourceResponse](https://github.com/sysid/sse-starlette)
 
-Server Sent Response implements the ServerSentEvent Protocol: 
https://www.w3.org/TR/2009/WD-eventsource-20090421.  
-It enables event streaming from the server to the client without the 
complexity of websockets.
+A response class that implements [Server-Sent 
Events](https://html.spec.whatwg.org/multipage/server-sent-events.html). It 
enables event streaming from the server to the client without the complexity of 
websockets.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/docs/routing.md 
new/starlette-0.17.1/docs/routing.md
--- old/starlette-0.16.0/docs/routing.md        2021-07-19 09:08:36.000000000 
+0200
+++ new/starlette-0.17.1/docs/routing.md        2021-11-17 17:10:27.000000000 
+0100
@@ -178,6 +178,50 @@
 url = app.url_path_for("user_detail", username=...)
 ```
 
+## Host-based routing
+
+If you want to use different routes for the same path based on the `Host` 
header.
+
+Note that port is removed from the `Host` header when matching.
+For example, `Host (host='example.org:3600', ...)` will not be processed 
+even if the `Host` header is `example.org:3600`. 
+Therefore, specify only the domain or IP address
+
+There are several ways to connect host-based routes to your application
+
+```python
+site = Router()  # Use eg. `@site.route()` to configure this.
+api = Router()  # Use eg. `@api.route()` to configure this.
+news = Router()  # Use eg. `@news.route()` to configure this.
+
+routes = [
+    Host('api.example.org', api, name="site_api")
+]
+
+app = Starlette(routes=routes)
+
+app.host('www.example.org', site, name="main_site")
+
+news_host = Host('news.example.org', news)
+app.router.routes.append(news_host)
+```
+
+URL lookups can include host parameters just like path parameters
+
+```python
+routes = [
+    Host("{subdomain}.example.org", name="sub", app=Router(routes=[
+        Mount("/users", name="users", routes=[
+            Route("/", user, name="user_list"),
+            Route("/{username}", user, name="user_detail")
+        ])
+    ]))
+]
+...
+url = request.url_for("sub:users:user_detail", username=..., subdomain=...)
+url = request.url_for("sub:users:user_list", subdomain=...)
+```
+
 ## Route priority
 
 Incoming paths are matched against each `Route` in order.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/docs/schemas.md 
new/starlette-0.17.1/docs/schemas.md
--- old/starlette-0.16.0/docs/schemas.md        2021-07-19 09:08:36.000000000 
+0200
+++ new/starlette-0.17.1/docs/schemas.md        2021-11-17 17:10:27.000000000 
+0100
@@ -51,7 +51,7 @@
     Route("/schema", endpoint=openapi_schema, include_in_schema=False)
 ]
 
-app = Starlette()
+app = Starlette(routes=routes)
 ```
 
 We can now access an OpenAPI schema at the "/schema" endpoint.
Binary files old/starlette-0.16.0/docs/sponsors/fastapi.png and 
new/starlette-0.17.1/docs/sponsors/fastapi.png differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/docs/third-party-packages.md 
new/starlette-0.17.1/docs/third-party-packages.md
--- old/starlette-0.16.0/docs/third-party-packages.md   2021-07-19 
09:08:36.000000000 +0200
+++ new/starlette-0.17.1/docs/third-party-packages.md   2021-11-17 
17:10:27.000000000 +0100
@@ -20,7 +20,7 @@
 Document your REST API built with Starlette by declaring OpenAPI (Swagger)
 schemas in YAML format in your endpoint's docstrings.
 
-### SpecTree 
+### SpecTree
 
 <a href="https://github.com/0b01001001/spectree"; target="_blank">GitHub</a>
 
@@ -43,8 +43,8 @@
 
 <a href="https://github.com/Sobolev5/channel-box"; target="_blank">GitHub</a>
 
-Another solution for websocket broadcast. Send messages to channel groups from 
any part of your code. 
-Checkout <a href="https://svue-backend.andrey-sobolev.ru/chat/chat1/"; 
target="_blank">channel-box-chat</a>, a simple chat application built using 
`channel-box` and `starlette`.
+Another solution for websocket broadcast. Send messages to channel groups from 
any part of your code.
+Checkout <a href="https://channel-box.andrey-sobolev.ru/"; 
target="_blank">MySimpleChat</a>, a simple chat application built using 
`channel-box` and `starlette`.
 
 ### Scout APM
 
@@ -90,6 +90,28 @@
 Middleware for Starlette that allows you to store and access the context data 
of a request.
 Can be used with logging so logs automatically use request headers such as 
x-request-id or x-correlation-id.
 
+
+### Starsessions
+
+<a href="https://github.com/alex-oleshkevich/starsessions"; 
target="_blank">GitHub</a>
+
+An alternate session support implementation with customizable storage backends.
+
+
+### Starlette Cramjam
+
+<a href="https://github.com/developmentseed/starlette-cramjam"; 
target="_blank">GitHub</a>
+
+A Starlette middleware that allows **brotli**, **gzip** and **deflate** 
compression algorithm with a minimal requirements.
+
+
+### Imia
+    
+<a href="https://github.com/alex-oleshkevich/imia"; target="_blank">GitHub</a>
+
+An authentication framework for Starlette with pluggable authenticators and 
login/logout flow.
+
+
 ## Frameworks
 
 ### Responder
@@ -116,3 +138,9 @@
 Formerly Starlette API.
 
 Flama aims to bring a layer on top of Starlette to provide an **easy to 
learn** and **fast to develop** approach for building **highly performant** 
GraphQL and REST APIs. In the same way of Starlette is, Flama is a perfect 
option for developing **asynchronous** and **production-ready** services.
+
+### Starlette-apps
+
+Roll your own framework with a simple app system, like 
[Django-GDAPS](https://gdaps.readthedocs.io/en/latest/) or 
[CakePHP](https://cakephp.org/).
+
+<a href="https://github.com/yourlabs/starlette-apps"; target="_blank">GitHub</a>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/mkdocs.yml 
new/starlette-0.17.1/mkdocs.yml
--- old/starlette-0.16.0/mkdocs.yml     2021-07-19 09:08:36.000000000 +0200
+++ new/starlette-0.17.1/mkdocs.yml     2021-11-17 17:10:27.000000000 +0100
@@ -4,6 +4,19 @@
 
 theme:
   name: 'material'
+  custom_dir: docs/overrides
+  palette:
+    - scheme: 'default'
+      media: '(prefers-color-scheme: light)'
+      toggle:
+        icon: 'material/lightbulb'
+        name: "Switch to dark mode"
+    - scheme: 'slate'
+      media: '(prefers-color-scheme: dark)'
+      primary: 'blue'
+      toggle:
+        icon: 'material/lightbulb-outline'
+        name: 'Switch to light mode'
 
 repo_name: encode/starlette
 repo_url: https://github.com/encode/starlette
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/requirements.txt 
new/starlette-0.17.1/requirements.txt
--- old/starlette-0.16.0/requirements.txt       2021-07-19 09:08:36.000000000 
+0200
+++ new/starlette-0.17.1/requirements.txt       2021-11-17 17:10:27.000000000 
+0100
@@ -1,5 +1,4 @@
 # Optionals
-graphene; python_version<'3.10'
 itsdangerous
 jinja2
 python-multipart
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/setup.cfg 
new/starlette-0.17.1/setup.cfg
--- old/starlette-0.16.0/setup.cfg      2021-07-19 09:08:36.000000000 +0200
+++ new/starlette-0.17.1/setup.cfg      2021-11-17 17:10:27.000000000 +0100
@@ -24,16 +24,11 @@
 filterwarnings=
     # Turn warnings that aren't filtered into exceptions
     error
-    # Deprecated GraphQL (including 
https://github.com/graphql-python/graphene/issues/1055)
-    ignore: GraphQLApp is deprecated and will be removed in a future 
release\..*:DeprecationWarning
     ignore: Using or importing the ABCs from 'collections' instead of from 
'collections\.abc' is deprecated.*:DeprecationWarning
     ignore: The 'context' alias has been deprecated. Please use 
'context_value' instead\.:DeprecationWarning
     ignore: The 'variables' alias has been deprecated. Please use 
'variable_values' instead\.:DeprecationWarning
+    # Workaround for Python 3.9.7 (see https://bugs.python.org/issue45097)
+    ignore:The loop argument is deprecated since Python 3\.8, and scheduled 
for removal in Python 3\.10\.:DeprecationWarning:asyncio
 
 [coverage:run]
 source_pkgs = starlette, tests
-# GraphQLApp incompatible with and untested on Python 3.10. It's deprecated, 
let's just ignore
-# coverage for it until it's gone.
-omit =
-  starlette/graphql.py
-  tests/test_graphql.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/setup.py 
new/starlette-0.17.1/setup.py
--- old/starlette-0.16.0/setup.py       2021-07-19 09:08:36.000000000 +0200
+++ new/starlette-0.17.1/setup.py       2021-11-17 17:10:27.000000000 +0100
@@ -44,7 +44,6 @@
     ],
     extras_require={
         "full": [
-            "graphene; python_version<'3.10'",
             "itsdangerous",
             "jinja2",
             "python-multipart",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/starlette/__init__.py 
new/starlette-0.17.1/starlette/__init__.py
--- old/starlette-0.16.0/starlette/__init__.py  2021-07-19 09:08:36.000000000 
+0200
+++ new/starlette-0.17.1/starlette/__init__.py  2021-11-17 17:10:27.000000000 
+0100
@@ -1 +1 @@
-__version__ = "0.16.0"
+__version__ = "0.17.1"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/starlette/authentication.py 
new/starlette-0.17.1/starlette/authentication.py
--- old/starlette-0.16.0/starlette/authentication.py    2021-07-19 
09:08:36.000000000 +0200
+++ new/starlette-0.17.1/starlette/authentication.py    2021-11-17 
17:10:27.000000000 +0100
@@ -40,7 +40,9 @@
             async def websocket_wrapper(
                 *args: typing.Any, **kwargs: typing.Any
             ) -> None:
-                websocket = kwargs.get("websocket", args[idx] if args else 
None)
+                websocket = kwargs.get(
+                    "websocket", args[idx] if idx < len(args) else None
+                )
                 assert isinstance(websocket, WebSocket)
 
                 if not has_required_scope(websocket, scopes_list):
@@ -56,7 +58,7 @@
             async def async_wrapper(
                 *args: typing.Any, **kwargs: typing.Any
             ) -> Response:
-                request = kwargs.get("request", args[idx] if args else None)
+                request = kwargs.get("request", args[idx] if idx < len(args) 
else None)
                 assert isinstance(request, Request)
 
                 if not has_required_scope(request, scopes_list):
@@ -73,7 +75,7 @@
             # Handle sync request/response functions.
             @functools.wraps(func)
             def sync_wrapper(*args: typing.Any, **kwargs: typing.Any) -> 
Response:
-                request = kwargs.get("request", args[idx] if args else None)
+                request = kwargs.get("request", args[idx] if idx < len(args) 
else None)
                 assert isinstance(request, Request)
 
                 if not has_required_scope(request, scopes_list):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/starlette/config.py 
new/starlette-0.17.1/starlette/config.py
--- old/starlette-0.16.0/starlette/config.py    2021-07-19 09:08:36.000000000 
+0200
+++ new/starlette-0.17.1/starlette/config.py    2021-11-17 17:10:27.000000000 
+0100
@@ -46,6 +46,8 @@
 
 environ = Environ()
 
+T = typing.TypeVar("T")
+
 
 class Config:
     def __init__(
@@ -58,6 +60,24 @@
         if env_file is not None and os.path.isfile(env_file):
             self.file_values = self._read_file(env_file)
 
+    @typing.overload
+    def __call__(
+        self, key: str, cast: typing.Type[T], default: T = ...
+    ) -> T:  # pragma: no cover
+        ...
+
+    @typing.overload
+    def __call__(
+        self, key: str, cast: typing.Type[str] = ..., default: str = ...
+    ) -> str:  # pragma: no cover
+        ...
+
+    @typing.overload
+    def __call__(
+        self, key: str, cast: typing.Type[str] = ..., default: T = ...
+    ) -> typing.Union[T, str]:  # pragma: no cover
+        ...
+
     def __call__(
         self, key: str, cast: typing.Callable = None, default: typing.Any = 
undefined
     ) -> typing.Any:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/starlette/datastructures.py 
new/starlette-0.17.1/starlette/datastructures.py
--- old/starlette-0.16.0/starlette/datastructures.py    2021-07-19 
09:08:36.000000000 +0200
+++ new/starlette-0.17.1/starlette/datastructures.py    2021-11-17 
17:10:27.000000000 +0100
@@ -266,7 +266,7 @@
         self._dict = {k: v for k, v in _items}
         self._list = _items
 
-    def getlist(self, key: typing.Any) -> typing.List[str]:
+    def getlist(self, key: typing.Any) -> typing.List[typing.Any]:
         return [item_value for item_key, item_value in self._list if item_key 
== key]
 
     def keys(self) -> typing.KeysView:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/starlette/graphql.py 
new/starlette-0.17.1/starlette/graphql.py
--- old/starlette-0.16.0/starlette/graphql.py   2021-07-19 09:08:36.000000000 
+0200
+++ new/starlette-0.17.1/starlette/graphql.py   1970-01-01 01:00:00.000000000 
+0100
@@ -1,275 +0,0 @@
-import json
-import typing
-import warnings
-
-from starlette import status
-from starlette.background import BackgroundTasks
-from starlette.concurrency import run_in_threadpool
-from starlette.requests import Request
-from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse, 
Response
-from starlette.types import Receive, Scope, Send
-
-warnings.warn(
-    "GraphQLApp is deprecated and will be removed in a future release. "
-    "Consider using a third-party GraphQL implementation. "
-    "See https://github.com/encode/starlette/issues/619.";,
-    DeprecationWarning,
-)
-
-try:
-    import graphene
-    from graphql.error import GraphQLError, format_error as 
format_graphql_error
-    from graphql.execution.executors.asyncio import AsyncioExecutor
-except ImportError:  # pragma: nocover
-    graphene = None
-    AsyncioExecutor = None  # type: ignore
-    format_graphql_error = None  # type: ignore
-    GraphQLError = None  # type: ignore
-
-
-class GraphQLApp:
-    def __init__(
-        self,
-        schema: "graphene.Schema",
-        executor_class: type = None,
-        graphiql: bool = True,
-    ) -> None:
-        self.schema = schema
-        self.graphiql = graphiql
-        self.executor_class = executor_class
-        self.is_async = executor_class is not None and issubclass(
-            executor_class, AsyncioExecutor
-        )
-
-    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> 
None:
-        if self.executor_class is not None:
-            self.executor = self.executor_class()
-
-        request = Request(scope, receive=receive)
-        response = await self.handle_graphql(request)
-        await response(scope, receive, send)
-
-    async def handle_graphql(self, request: Request) -> Response:
-        if request.method in ("GET", "HEAD"):
-            if "text/html" in request.headers.get("Accept", ""):
-                if not self.graphiql:
-                    return PlainTextResponse(
-                        "Not Found", status_code=status.HTTP_404_NOT_FOUND
-                    )
-                return await self.handle_graphiql(request)
-
-            data: typing.Mapping[str, typing.Any] = request.query_params
-
-        elif request.method == "POST":
-            content_type = request.headers.get("Content-Type", "")
-
-            if "application/json" in content_type:
-                data = await request.json()
-            elif "application/graphql" in content_type:
-                body = await request.body()
-                text = body.decode()
-                data = {"query": text}
-            elif "query" in request.query_params:
-                data = request.query_params
-            else:
-                return PlainTextResponse(
-                    "Unsupported Media Type",
-                    status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
-                )
-
-        else:
-            return PlainTextResponse(
-                "Method Not Allowed", 
status_code=status.HTTP_405_METHOD_NOT_ALLOWED
-            )
-
-        try:
-            query = data["query"]
-            variables = data.get("variables")
-            operation_name = data.get("operationName")
-        except KeyError:
-            return PlainTextResponse(
-                "No GraphQL query found in the request",
-                status_code=status.HTTP_400_BAD_REQUEST,
-            )
-
-        background = BackgroundTasks()
-        context = {"request": request, "background": background}
-
-        result = await self.execute(
-            query, variables=variables, context=context, 
operation_name=operation_name
-        )
-        error_data = (
-            [format_graphql_error(err) for err in result.errors]
-            if result.errors
-            else None
-        )
-        response_data = {"data": result.data}
-        if error_data:
-            response_data["errors"] = error_data
-        status_code = (
-            status.HTTP_400_BAD_REQUEST if result.errors else 
status.HTTP_200_OK
-        )
-
-        return JSONResponse(
-            response_data, status_code=status_code, background=background
-        )
-
-    async def execute(  # type: ignore
-        self, query, variables=None, context=None, operation_name=None
-    ):
-        if self.is_async:
-            return await self.schema.execute(
-                query,
-                variables=variables,
-                operation_name=operation_name,
-                executor=self.executor,
-                return_promise=True,
-                context=context,
-            )
-        else:
-            return await run_in_threadpool(
-                self.schema.execute,
-                query,
-                variables=variables,
-                operation_name=operation_name,
-                context=context,
-            )
-
-    async def handle_graphiql(self, request: Request) -> Response:
-        text = GRAPHIQL.replace("{{REQUEST_PATH}}", 
json.dumps(request.url.path))
-        return HTMLResponse(text)
-
-
-GRAPHIQL = """
-<!--
- *  Copyright (c) Facebook, Inc.
- *  All rights reserved.
- *
- *  This source code is licensed under the license found in the
- *  LICENSE file in the root directory of this source tree.
--->
-<!DOCTYPE html>
-<html>
-  <head>
-    <style>
-      body {
-        height: 100%;
-        margin: 0;
-        width: 100%;
-        overflow: hidden;
-      }
-      #graphiql {
-        height: 100vh;
-      }
-    </style>
-    <!--
-      This GraphiQL example depends on Promise and fetch, which are available 
in
-      modern browsers, but can be "polyfilled" for older browsers.
-      GraphiQL itself depends on React DOM.
-      If you do not want to rely on a CDN, you can host these files locally or
-      include them directly in your favored resource bunder.
-    -->
-    <link href="//cdn.jsdelivr.net/npm/[email protected]/graphiql.css" 
rel="stylesheet"/>
-    <script 
src="//cdn.jsdelivr.net/npm/[email protected]/fetch.min.js"></script>
-    <script 
src="//cdn.jsdelivr.net/npm/[email protected]/umd/react.production.min.js"></script>
-    <script 
src="//cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.production.min.js"></script>
-    <script 
src="//cdn.jsdelivr.net/npm/[email protected]/graphiql.min.js"></script>
-  </head>
-  <body>
-    <div id="graphiql">Loading...</div>
-    <script>
-      /**
-       * This GraphiQL example illustrates how to use some of GraphiQL's props
-       * in order to enable reading and updating the URL parameters, making
-       * link sharing of queries a little bit easier.
-       *
-       * This is only one example of this kind of feature, GraphiQL exposes
-       * various React params to enable interesting integrations.
-       */
-      // Parse the search string to get url parameters.
-      var search = window.location.search;
-      var parameters = {};
-      search.substr(1).split('&').forEach(function (entry) {
-        var eq = entry.indexOf('=');
-        if (eq >= 0) {
-          parameters[decodeURIComponent(entry.slice(0, eq))] =
-            decodeURIComponent(entry.slice(eq + 1));
-        }
-      });
-      // if variables was provided, try to format it.
-      if (parameters.variables) {
-        try {
-          parameters.variables =
-            JSON.stringify(JSON.parse(parameters.variables), null, 2);
-        } catch (e) {
-          // Do nothing, we want to display the invalid JSON as a string, 
rather
-          // than present an error.
-        }
-      }
-      // When the query and variables string is edited, update the URL bar so
-      // that it can be easily shared
-      function onEditQuery(newQuery) {
-        parameters.query = newQuery;
-        updateURL();
-      }
-      function onEditVariables(newVariables) {
-        parameters.variables = newVariables;
-        updateURL();
-      }
-      function onEditOperationName(newOperationName) {
-        parameters.operationName = newOperationName;
-        updateURL();
-      }
-      function updateURL() {
-        var newSearch = '?' + Object.keys(parameters).filter(function (key) {
-          return Boolean(parameters[key]);
-        }).map(function (key) {
-          return encodeURIComponent(key) + '=' +
-            encodeURIComponent(parameters[key]);
-        }).join('&');
-        history.replaceState(null, null, newSearch);
-      }
-      // Defines a GraphQL fetcher using the fetch API. You're not required to
-      // use fetch, and could instead implement graphQLFetcher however you 
like,
-      // as long as it returns a Promise or Observable.
-      function graphQLFetcher(graphQLParams) {
-        // This example expects a GraphQL server at the path /graphql.
-        // Change this to point wherever you host your GraphQL server.
-        return fetch({{REQUEST_PATH}}, {
-          method: 'post',
-          headers: {
-            'Accept': 'application/json',
-            'Content-Type': 'application/json',
-          },
-          body: JSON.stringify(graphQLParams),
-          credentials: 'include',
-        }).then(function (response) {
-          return response.text();
-        }).then(function (responseBody) {
-          try {
-            return JSON.parse(responseBody);
-          } catch (error) {
-            return responseBody;
-          }
-        });
-      }
-      // Render <GraphiQL /> into the body.
-      // See the README in the top level of this module to learn more about
-      // how you can customize GraphiQL by providing different values or
-      // additional child elements.
-      ReactDOM.render(
-        React.createElement(GraphiQL, {
-          fetcher: graphQLFetcher,
-          query: parameters.query,
-          variables: parameters.variables,
-          operationName: parameters.operationName,
-          onEditQuery: onEditQuery,
-          onEditVariables: onEditVariables,
-          onEditOperationName: onEditOperationName
-        }),
-        document.getElementById('graphiql')
-      );
-    </script>
-  </body>
-</html>
-"""  # noqa: E501
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/starlette/middleware/base.py 
new/starlette-0.17.1/starlette/middleware/base.py
--- old/starlette-0.16.0/starlette/middleware/base.py   2021-07-19 
09:08:36.000000000 +0200
+++ new/starlette-0.17.1/starlette/middleware/base.py   2021-11-17 
17:10:27.000000000 +0100
@@ -23,17 +23,25 @@
             return
 
         async def call_next(request: Request) -> Response:
+            app_exc: typing.Optional[Exception] = None
             send_stream, recv_stream = anyio.create_memory_object_stream()
 
             async def coro() -> None:
+                nonlocal app_exc
+
                 async with send_stream:
-                    await self.app(scope, request.receive, send_stream.send)
+                    try:
+                        await self.app(scope, request.receive, 
send_stream.send)
+                    except Exception as exc:
+                        app_exc = exc
 
             task_group.start_soon(coro)
 
             try:
                 message = await recv_stream.receive()
             except anyio.EndOfStream:
+                if app_exc is not None:
+                    raise app_exc
                 raise RuntimeError("No response returned.")
 
             assert message["type"] == "http.response.start"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/starlette/middleware/sessions.py 
new/starlette-0.17.1/starlette/middleware/sessions.py
--- old/starlette-0.16.0/starlette/middleware/sessions.py       2021-07-19 
09:08:36.000000000 +0200
+++ new/starlette-0.17.1/starlette/middleware/sessions.py       2021-11-17 
17:10:27.000000000 +0100
@@ -3,7 +3,7 @@
 from base64 import b64decode, b64encode
 
 import itsdangerous
-from itsdangerous.exc import BadTimeSignature, SignatureExpired
+from itsdangerous.exc import BadSignature
 
 from starlette.datastructures import MutableHeaders, Secret
 from starlette.requests import HTTPConnection
@@ -42,7 +42,7 @@
                 data = self.signer.unsign(data, max_age=self.max_age)
                 scope["session"] = json.loads(b64decode(data))
                 initial_session_was_empty = False
-            except (BadTimeSignature, SignatureExpired):
+            except BadSignature:
                 scope["session"] = {}
         else:
             scope["session"] = {}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/starlette/requests.py 
new/starlette-0.17.1/starlette/requests.py
--- old/starlette-0.16.0/starlette/requests.py  2021-07-19 09:08:36.000000000 
+0200
+++ new/starlette-0.17.1/starlette/requests.py  2021-11-17 17:10:27.000000000 
+0100
@@ -65,7 +65,7 @@
         assert scope["type"] in ("http", "websocket")
         self.scope = scope
 
-    def __getitem__(self, key: str) -> str:
+    def __getitem__(self, key: str) -> typing.Any:
         return self.scope[key]
 
     def __iter__(self) -> typing.Iterator[str]:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/starlette/responses.py 
new/starlette-0.17.1/starlette/responses.py
--- old/starlette-0.16.0/starlette/responses.py 2021-07-19 09:08:36.000000000 
+0200
+++ new/starlette-0.17.1/starlette/responses.py 2021-11-17 17:10:27.000000000 
+0100
@@ -125,8 +125,25 @@
         cookie_val = cookie.output(header="").strip()
         self.raw_headers.append((b"set-cookie", cookie_val.encode("latin-1")))
 
-    def delete_cookie(self, key: str, path: str = "/", domain: str = None) -> 
None:
-        self.set_cookie(key, expires=0, max_age=0, path=path, domain=domain)
+    def delete_cookie(
+        self,
+        key: str,
+        path: str = "/",
+        domain: str = None,
+        secure: bool = False,
+        httponly: bool = False,
+        samesite: str = "lax",
+    ) -> None:
+        self.set_cookie(
+            key,
+            max_age=0,
+            expires=0,
+            path=path,
+            domain=domain,
+            secure=secure,
+            httponly=httponly,
+            samesite=samesite,
+        )
 
     async def __call__(self, scope: Scope, receive: Receive, send: Send) -> 
None:
         await send(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/starlette/staticfiles.py 
new/starlette-0.17.1/starlette/staticfiles.py
--- old/starlette-0.16.0/starlette/staticfiles.py       2021-07-19 
09:08:36.000000000 +0200
+++ new/starlette-0.17.1/starlette/staticfiles.py       2021-11-17 
17:10:27.000000000 +0100
@@ -7,12 +7,8 @@
 import anyio
 
 from starlette.datastructures import URL, Headers
-from starlette.responses import (
-    FileResponse,
-    PlainTextResponse,
-    RedirectResponse,
-    Response,
-)
+from starlette.exceptions import HTTPException
+from starlette.responses import FileResponse, RedirectResponse, Response
 from starlette.types import Receive, Scope, Send
 
 PathLike = typing.Union[str, "os.PathLike[str]"]
@@ -109,9 +105,16 @@
         Returns an HTTP response, given the incoming path, method and request 
headers.
         """
         if scope["method"] not in ("GET", "HEAD"):
-            return PlainTextResponse("Method Not Allowed", status_code=405)
+            raise HTTPException(status_code=405)
 
-        full_path, stat_result = await self.lookup_path(path)
+        try:
+            full_path, stat_result = await anyio.to_thread.run_sync(
+                self.lookup_path, path
+            )
+        except PermissionError:
+            raise HTTPException(status_code=401)
+        except OSError:
+            raise
 
         if stat_result and stat.S_ISREG(stat_result.st_mode):
             # We have a static file to serve.
@@ -121,7 +124,9 @@
             # We're in HTML mode, and have got a directory URL.
             # Check if we have 'index.html' file to serve.
             index_path = os.path.join(path, "index.html")
-            full_path, stat_result = await self.lookup_path(index_path)
+            full_path, stat_result = await anyio.to_thread.run_sync(
+                self.lookup_path, index_path
+            )
             if stat_result is not None and stat.S_ISREG(stat_result.st_mode):
                 if not scope["path"].endswith("/"):
                     # Directory URLs should redirect to always end in "/".
@@ -132,18 +137,19 @@
 
         if self.html:
             # Check for '404.html' if we're in HTML mode.
-            full_path, stat_result = await self.lookup_path("404.html")
-            if stat_result is not None and stat.S_ISREG(stat_result.st_mode):
+            full_path, stat_result = await anyio.to_thread.run_sync(
+                self.lookup_path, "404.html"
+            )
+            if stat_result and stat.S_ISREG(stat_result.st_mode):
                 return FileResponse(
                     full_path,
                     stat_result=stat_result,
                     method=scope["method"],
                     status_code=404,
                 )
+        raise HTTPException(status_code=404)
 
-        return PlainTextResponse("Not Found", status_code=404)
-
-    async def lookup_path(
+    def lookup_path(
         self, path: str
     ) -> typing.Tuple[str, typing.Optional[os.stat_result]]:
         for directory in self.all_directories:
@@ -154,10 +160,9 @@
                 # directory.
                 continue
             try:
-                stat_result = await anyio.to_thread.run_sync(os.stat, 
full_path)
-                return full_path, stat_result
-            except FileNotFoundError:
-                pass
+                return full_path, os.stat(full_path)
+            except (FileNotFoundError, NotADirectoryError):
+                continue
         return "", None
 
     def file_response(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/starlette/templating.py 
new/starlette-0.17.1/starlette/templating.py
--- old/starlette-0.16.0/starlette/templating.py        2021-07-19 
09:08:36.000000000 +0200
+++ new/starlette-0.17.1/starlette/templating.py        2021-11-17 
17:10:27.000000000 +0100
@@ -1,4 +1,5 @@
 import typing
+from os import PathLike
 
 from starlette.background import BackgroundTask
 from starlette.responses import Response
@@ -54,11 +55,13 @@
     return templates.TemplateResponse("index.html", {"request": request})
     """
 
-    def __init__(self, directory: str) -> None:
+    def __init__(self, directory: typing.Union[str, PathLike]) -> None:
         assert jinja2 is not None, "jinja2 must be installed to use 
Jinja2Templates"
         self.env = self._create_env(directory)
 
-    def _create_env(self, directory: str) -> "jinja2.Environment":
+    def _create_env(
+        self, directory: typing.Union[str, PathLike]
+    ) -> "jinja2.Environment":
         @pass_context
         def url_for(context: dict, name: str, **path_params: typing.Any) -> 
str:
             request = context["request"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/tests/conftest.py 
new/starlette-0.17.1/tests/conftest.py
--- old/starlette-0.16.0/tests/conftest.py      2021-07-19 09:08:36.000000000 
+0200
+++ new/starlette-0.17.1/tests/conftest.py      2021-11-17 17:10:27.000000000 
+0100
@@ -1,12 +1,9 @@
 import functools
-import sys
 
 import pytest
 
 from starlette.testclient import TestClient
 
-collect_ignore = ["test_graphql.py"] if sys.version_info >= (3, 10) else []
-
 
 @pytest.fixture
 def no_trio_support(anyio_backend_name):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/tests/middleware/test_base.py 
new/starlette-0.17.1/tests/middleware/test_base.py
--- old/starlette-0.16.0/tests/middleware/test_base.py  2021-07-19 
09:08:36.000000000 +0200
+++ new/starlette-0.17.1/tests/middleware/test_base.py  2021-11-17 
17:10:27.000000000 +0100
@@ -25,7 +25,7 @@
 
 @app.route("/exc")
 def exc(request):
-    raise Exception()
+    raise Exception("Exc")
 
 
 @app.route("/no-response")
@@ -52,8 +52,9 @@
     response = client.get("/")
     assert response.headers["Custom-Header"] == "Example"
 
-    with pytest.raises(Exception):
+    with pytest.raises(Exception) as ctx:
         response = client.get("/exc")
+    assert str(ctx.value) == "Exc"
 
     with pytest.raises(RuntimeError):
         response = client.get("/no-response")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/tests/middleware/test_session.py 
new/starlette-0.17.1/tests/middleware/test_session.py
--- old/starlette-0.16.0/tests/middleware/test_session.py       2021-07-19 
09:08:36.000000000 +0200
+++ new/starlette-0.17.1/tests/middleware/test_session.py       2021-11-17 
17:10:27.000000000 +0100
@@ -112,3 +112,16 @@
     cookie = response.headers["set-cookie"]
     cookie_path = re.search(r"; path=(\S+);", cookie).groups()[0]
     assert cookie_path == "/second_app"
+
+
+def test_invalid_session_cookie(test_client_factory):
+    app = create_app()
+    app.add_middleware(SessionMiddleware, secret_key="example")
+    client = test_client_factory(app)
+
+    response = client.post("/update_session", json={"some": "data"})
+    assert response.json() == {"session": {"some": "data"}}
+
+    # we expect it to not raise an exception if we provide a bogus session 
cookie
+    response = client.get("/view_session", cookies={"session": "invalid"})
+    assert response.json() == {"session": {}}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/tests/test_database.py 
new/starlette-0.17.1/tests/test_database.py
--- old/starlette-0.16.0/tests/test_database.py 2021-07-19 09:08:36.000000000 
+0200
+++ new/starlette-0.17.1/tests/test_database.py 2021-11-17 17:10:27.000000000 
+0100
@@ -126,8 +126,6 @@
 
 def test_database_execute_many(test_client_factory):
     with test_client_factory(app) as client:
-        response = client.get("/notes")
-
         data = [
             {"text": "buy the milk", "completed": True},
             {"text": "walk the dog", "completed": False},
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/tests/test_graphql.py 
new/starlette-0.17.1/tests/test_graphql.py
--- old/starlette-0.16.0/tests/test_graphql.py  2021-07-19 09:08:36.000000000 
+0200
+++ new/starlette-0.17.1/tests/test_graphql.py  1970-01-01 01:00:00.000000000 
+0100
@@ -1,152 +0,0 @@
-import graphene
-import pytest
-from graphql.execution.executors.asyncio import AsyncioExecutor
-
-from starlette.applications import Starlette
-from starlette.datastructures import Headers
-from starlette.graphql import GraphQLApp
-
-
-class FakeAuthMiddleware:
-    def __init__(self, app) -> None:
-        self.app = app
-
-    async def __call__(self, scope, receive, send):
-        headers = Headers(scope=scope)
-        scope["user"] = "Jane" if headers.get("Authorization") == "Bearer 123" 
else None
-        await self.app(scope, receive, send)
-
-
-class Query(graphene.ObjectType):
-    hello = graphene.String(name=graphene.String(default_value="stranger"))
-    whoami = graphene.String()
-
-    def resolve_hello(self, info, name):
-        return "Hello " + name
-
-    def resolve_whoami(self, info):
-        return (
-            "a mystery"
-            if info.context["request"]["user"] is None
-            else info.context["request"]["user"]
-        )
-
-
-schema = graphene.Schema(query=Query)
-
-
[email protected]
-def client(test_client_factory):
-    app = GraphQLApp(schema=schema, graphiql=True)
-    return test_client_factory(app)
-
-
-def test_graphql_get(client):
-    response = client.get("/?query={ hello }")
-    assert response.status_code == 200
-    assert response.json() == {"data": {"hello": "Hello stranger"}}
-
-
-def test_graphql_post(client):
-    response = client.post("/?query={ hello }")
-    assert response.status_code == 200
-    assert response.json() == {"data": {"hello": "Hello stranger"}}
-
-
-def test_graphql_post_json(client):
-    response = client.post("/", json={"query": "{ hello }"})
-    assert response.status_code == 200
-    assert response.json() == {"data": {"hello": "Hello stranger"}}
-
-
-def test_graphql_post_graphql(client):
-    response = client.post(
-        "/", data="{ hello }", headers={"content-type": "application/graphql"}
-    )
-    assert response.status_code == 200
-    assert response.json() == {"data": {"hello": "Hello stranger"}}
-
-
-def test_graphql_post_invalid_media_type(client):
-    response = client.post("/", data="{ hello }", headers={"content-type": 
"dummy"})
-    assert response.status_code == 415
-    assert response.text == "Unsupported Media Type"
-
-
-def test_graphql_put(client):
-    response = client.put("/", json={"query": "{ hello }"})
-    assert response.status_code == 405
-    assert response.text == "Method Not Allowed"
-
-
-def test_graphql_no_query(client):
-    response = client.get("/")
-    assert response.status_code == 400
-    assert response.text == "No GraphQL query found in the request"
-
-
-def test_graphql_invalid_field(client):
-    response = client.post("/", json={"query": "{ dummy }"})
-    assert response.status_code == 400
-    assert response.json() == {
-        "data": None,
-        "errors": [
-            {
-                "locations": [{"column": 3, "line": 1}],
-                "message": 'Cannot query field "dummy" on type "Query".',
-            }
-        ],
-    }
-
-
-def test_graphiql_get(client):
-    response = client.get("/", headers={"accept": "text/html"})
-    assert response.status_code == 200
-    assert "<!DOCTYPE html>" in response.text
-
-
-def test_graphiql_not_found(test_client_factory):
-    app = GraphQLApp(schema=schema, graphiql=False)
-    client = test_client_factory(app)
-    response = client.get("/", headers={"accept": "text/html"})
-    assert response.status_code == 404
-    assert response.text == "Not Found"
-
-
-def test_add_graphql_route(test_client_factory):
-    app = Starlette()
-    app.add_route("/", GraphQLApp(schema=schema))
-    client = test_client_factory(app)
-    response = client.get("/?query={ hello }")
-    assert response.status_code == 200
-    assert response.json() == {"data": {"hello": "Hello stranger"}}
-
-
-def test_graphql_context(test_client_factory):
-    app = Starlette()
-    app.add_middleware(FakeAuthMiddleware)
-    app.add_route("/", GraphQLApp(schema=schema))
-    client = test_client_factory(app)
-    response = client.post(
-        "/", json={"query": "{ whoami }"}, headers={"Authorization": "Bearer 
123"}
-    )
-    assert response.status_code == 200
-    assert response.json() == {"data": {"whoami": "Jane"}}
-
-
-class ASyncQuery(graphene.ObjectType):
-    hello = graphene.String(name=graphene.String(default_value="stranger"))
-
-    async def resolve_hello(self, info, name):
-        return "Hello " + name
-
-
-async_schema = graphene.Schema(query=ASyncQuery)
-async_app = GraphQLApp(schema=async_schema, executor_class=AsyncioExecutor)
-
-
-def test_graphql_async(no_trio_support, test_client_factory):
-    client = test_client_factory(async_app)
-    response = client.get("/?query={ hello }")
-    assert response.status_code == 200
-    assert response.json() == {"data": {"hello": "Hello stranger"}}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/starlette-0.16.0/tests/test_staticfiles.py 
new/starlette-0.17.1/tests/test_staticfiles.py
--- old/starlette-0.16.0/tests/test_staticfiles.py      2021-07-19 
09:08:36.000000000 +0200
+++ new/starlette-0.17.1/tests/test_staticfiles.py      2021-11-17 
17:10:27.000000000 +0100
@@ -1,11 +1,13 @@
 import os
 import pathlib
+import stat
 import time
 
 import anyio
 import pytest
 
 from starlette.applications import Starlette
+from starlette.exceptions import HTTPException
 from starlette.requests import Request
 from starlette.routing import Mount
 from starlette.staticfiles import StaticFiles
@@ -71,8 +73,10 @@
     with open(path, "w") as file:
         file.write("<file content>")
 
-    app = StaticFiles(directory=tmpdir)
+    routes = [Mount("/", app=StaticFiles(directory=tmpdir), name="static")]
+    app = Starlette(routes=routes)
     client = test_client_factory(app)
+
     response = client.post("/example.txt")
     assert response.status_code == 405
     assert response.text == "Method Not Allowed"
@@ -83,8 +87,10 @@
     with open(path, "w") as file:
         file.write("<file content>")
 
-    app = StaticFiles(directory=tmpdir)
+    routes = [Mount("/", app=StaticFiles(directory=tmpdir), name="static")]
+    app = Starlette(routes=routes)
     client = test_client_factory(app)
+
     response = client.get("/")
     assert response.status_code == 404
     assert response.text == "Not Found"
@@ -95,8 +101,10 @@
     with open(path, "w") as file:
         file.write("<file content>")
 
-    app = StaticFiles(directory=tmpdir)
+    routes = [Mount("/", app=StaticFiles(directory=tmpdir), name="static")]
+    app = Starlette(routes=routes)
     client = test_client_factory(app)
+
     response = client.get("/404.txt")
     assert response.status_code == 404
     assert response.text == "Not Found"
@@ -136,11 +144,15 @@
     app = StaticFiles(directory=tmpdir)
     client = test_client_factory(app)
     assert not app.config_checked
-    client.get("/")
-    assert app.config_checked
-    client.get("/")
+
+    with pytest.raises(HTTPException):
+        client.get("/")
+
     assert app.config_checked
 
+    with pytest.raises(HTTPException):
+        client.get("/")
+
 
 def test_staticfiles_prevents_breaking_out_of_directory(tmpdir):
     directory = os.path.join(tmpdir, "foo")
@@ -154,9 +166,12 @@
     # We can't test this with 'requests', so we test the app directly here.
     path = app.get_path({"path": "/../example.txt"})
     scope = {"method": "GET"}
-    response = anyio.run(app.get_response, path, scope)
-    assert response.status_code == 404
-    assert response.body == b"Not Found"
+
+    with pytest.raises(HTTPException) as exc_info:
+        anyio.run(app.get_response, path, scope)
+
+    assert exc_info.value.status_code == 404
+    assert exc_info.value.detail == "Not Found"
 
 
 def test_staticfiles_never_read_file_for_head_method(tmpdir, 
test_client_factory):
@@ -214,7 +229,7 @@
     assert response.content == b"<file content>"
 
 
-def test_staticfiles_html(tmpdir, test_client_factory):
+def test_staticfiles_html_normal(tmpdir, test_client_factory):
     path = os.path.join(tmpdir, "404.html")
     with open(path, "w") as file:
         file.write("<h1>Custom not found page</h1>")
@@ -247,6 +262,73 @@
     assert response.text == "<h1>Custom not found page</h1>"
 
 
+def test_staticfiles_html_without_index(tmpdir, test_client_factory):
+    path = os.path.join(tmpdir, "404.html")
+    with open(path, "w") as file:
+        file.write("<h1>Custom not found page</h1>")
+    path = os.path.join(tmpdir, "dir")
+    os.mkdir(path)
+
+    app = StaticFiles(directory=tmpdir, html=True)
+    client = test_client_factory(app)
+
+    response = client.get("/dir/")
+    assert response.url == "http://testserver/dir/";
+    assert response.status_code == 404
+    assert response.text == "<h1>Custom not found page</h1>"
+
+    response = client.get("/dir")
+    assert response.url == "http://testserver/dir";
+    assert response.status_code == 404
+    assert response.text == "<h1>Custom not found page</h1>"
+
+    response = client.get("/missing")
+    assert response.status_code == 404
+    assert response.text == "<h1>Custom not found page</h1>"
+
+
+def test_staticfiles_html_without_404(tmpdir, test_client_factory):
+    path = os.path.join(tmpdir, "dir")
+    os.mkdir(path)
+    path = os.path.join(path, "index.html")
+    with open(path, "w") as file:
+        file.write("<h1>Hello</h1>")
+
+    app = StaticFiles(directory=tmpdir, html=True)
+    client = test_client_factory(app)
+
+    response = client.get("/dir/")
+    assert response.url == "http://testserver/dir/";
+    assert response.status_code == 200
+    assert response.text == "<h1>Hello</h1>"
+
+    response = client.get("/dir")
+    assert response.url == "http://testserver/dir/";
+    assert response.status_code == 200
+    assert response.text == "<h1>Hello</h1>"
+
+    with pytest.raises(HTTPException) as exc_info:
+        response = client.get("/missing")
+    assert exc_info.value.status_code == 404
+
+
+def test_staticfiles_html_only_files(tmpdir, test_client_factory):
+    path = os.path.join(tmpdir, "hello.html")
+    with open(path, "w") as file:
+        file.write("<h1>Hello</h1>")
+
+    app = StaticFiles(directory=tmpdir, html=True)
+    client = test_client_factory(app)
+
+    with pytest.raises(HTTPException) as exc_info:
+        response = client.get("/")
+    assert exc_info.value.status_code == 404
+
+    response = client.get("/hello.html")
+    assert response.status_code == 200
+    assert response.text == "<h1>Hello</h1>"
+
+
 def test_staticfiles_cache_invalidation_for_deleted_file_html_mode(
     tmpdir, test_client_factory
 ):
@@ -284,3 +366,70 @@
     )
     assert resp_deleted.status_code == 404
     assert resp_deleted.text == "<p>404 file</p>"
+
+
+def test_staticfiles_with_invalid_dir_permissions_returns_401(
+    tmpdir, test_client_factory
+):
+    path = os.path.join(tmpdir, "example.txt")
+    with open(path, "w") as file:
+        file.write("<file content>")
+
+    os.chmod(tmpdir, stat.S_IRWXO)
+
+    routes = [Mount("/", app=StaticFiles(directory=tmpdir), name="static")]
+    app = Starlette(routes=routes)
+    client = test_client_factory(app)
+
+    response = client.get("/example.txt")
+    assert response.status_code == 401
+    assert response.text == "Unauthorized"
+
+
+def test_staticfiles_with_missing_dir_returns_404(tmpdir, test_client_factory):
+    path = os.path.join(tmpdir, "example.txt")
+    with open(path, "w") as file:
+        file.write("<file content>")
+
+    routes = [Mount("/", app=StaticFiles(directory=tmpdir), name="static")]
+    app = Starlette(routes=routes)
+    client = test_client_factory(app)
+
+    response = client.get("/foo/example.txt")
+    assert response.status_code == 404
+    assert response.text == "Not Found"
+
+
+def test_staticfiles_access_file_as_dir_returns_404(tmpdir, 
test_client_factory):
+    path = os.path.join(tmpdir, "example.txt")
+    with open(path, "w") as file:
+        file.write("<file content>")
+
+    routes = [Mount("/", app=StaticFiles(directory=tmpdir), name="static")]
+    app = Starlette(routes=routes)
+    client = test_client_factory(app)
+
+    response = client.get("/example.txt/foo")
+    assert response.status_code == 404
+    assert response.text == "Not Found"
+
+
+def test_staticfiles_unhandled_os_error_returns_500(
+    tmpdir, test_client_factory, monkeypatch
+):
+    def mock_timeout(*args, **kwargs):
+        raise TimeoutError
+
+    path = os.path.join(tmpdir, "example.txt")
+    with open(path, "w") as file:
+        file.write("<file content>")
+
+    routes = [Mount("/", app=StaticFiles(directory=tmpdir), name="static")]
+    app = Starlette(routes=routes)
+    client = test_client_factory(app, raise_server_exceptions=False)
+
+    monkeypatch.setattr("starlette.staticfiles.StaticFiles.lookup_path", 
mock_timeout)
+
+    response = client.get("/example.txt")
+    assert response.status_code == 500
+    assert response.text == "Internal Server Error"

Reply via email to