This is an automated email from the ASF dual-hosted git repository.

tn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git


The following commit(s) were added to refs/heads/main by this push:
     new c69be3d  remove asfquart source and replace it with a git dependency 
for now
c69be3d is described below

commit c69be3dc7d0608c0574e36ccaf4508e73cdd1227
Author: Thomas Neidhart <[email protected]>
AuthorDate: Wed Mar 19 20:50:58 2025 +0100

    remove asfquart source and replace it with a git dependency for now
---
 .gitignore                                         |   1 +
 Dockerfile.alpine                                  |   1 -
 asfquart/CHANGELOG.md                              |  23 --
 asfquart/LICENSE                                   | 201 -----------
 asfquart/README.md                                 |  73 ----
 asfquart/docs/auth.md                              |  68 ----
 asfquart/docs/oauth.md                             |  21 --
 asfquart/docs/readme.md                            |  30 --
 asfquart/docs/sessions.md                          |  69 ----
 asfquart/docs/templates.md                         |  61 ----
 .../examples/snippets/personal_access_tokens.py    |  55 ---
 asfquart/examples/snippets/roleaccounts.yaml       |   7 -
 asfquart/examples/snippets/simple_app.py           |  33 --
 asfquart/pyproject.toml                            |  53 ---
 asfquart/setup.cfg                                 |  12 -
 asfquart/src/asfquart/__init__.py                  |  21 --
 asfquart/src/asfquart/auth.py                      | 170 ---------
 asfquart/src/asfquart/base.py                      | 389 ---------------------
 asfquart/src/asfquart/config.py                    |  33 --
 asfquart/src/asfquart/generics.py                  | 129 -------
 asfquart/src/asfquart/ldap.py                      |  67 ----
 asfquart/src/asfquart/session.py                   | 119 -------
 asfquart/src/asfquart/utils.py                     | 129 -------
 asfquart/tests/auth.py                             | 129 -------
 asfquart/tests/config.py                           |  25 --
 asfquart/tests/data/config.test.yaml               |   7 -
 asfquart/tests/session.py                          |  17 -
 atr/blueprints/admin/__init__.py                   |   4 +-
 atr/blueprints/admin/admin.py                      |   4 +-
 atr/routes/__init__.py                             |   2 +-
 atr/routes/candidate.py                            |   8 +-
 atr/routes/dev.py                                  |   4 +-
 atr/routes/docs.py                                 |   4 +-
 atr/routes/download.py                             |   6 +-
 atr/routes/keys.py                                 |   8 +-
 atr/routes/package.py                              |   6 +-
 atr/routes/projects.py                             |   4 +-
 atr/routes/release.py                              |   6 +-
 atr/routes/vote_policy.py                          |   4 +-
 atr/server.py                                      |   8 +-
 poetry.lock                                        | 213 ++++++-----
 pyproject.toml                                     |  15 +-
 42 files changed, 136 insertions(+), 2103 deletions(-)

diff --git a/.gitignore b/.gitignore
index bc0586d..f9cff25 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,4 @@ __pycache__/
 apptoken.txt
 bootstrap/build/
 state/
+atr/_version.py
diff --git a/Dockerfile.alpine b/Dockerfile.alpine
index 2caa41b..c3af852 100644
--- a/Dockerfile.alpine
+++ b/Dockerfile.alpine
@@ -59,7 +59,6 @@ WORKDIR /opt/atr
 # copy app and wheels from builder
 COPY --from=builder /opt/atr/.venv ./.venv
 COPY --from=builder /opt/atr/atr ./atr
-COPY --from=builder /opt/atr/asfquart ./asfquart
 COPY --from=builder /opt/atr/migrations ./migrations
 COPY --from=builder /opt/atr/scripts ./scripts
 COPY --from=builder /opt/atr/Makefile .
diff --git a/asfquart/CHANGELOG.md b/asfquart/CHANGELOG.md
deleted file mode 100644
index b4f4d3b..0000000
--- a/asfquart/CHANGELOG.md
+++ /dev/null
@@ -1,23 +0,0 @@
-Changes in 0.1.10:
- - OAuth redirects have switch to using the Refresh HTTP header instead of a 
30x response, allowing
-   samesite cookies to work with external OAuth providers when the hostname 
differs.
-
-Changes in 0.1.9:
- - added the `metadata` dict to session objects where apps can store 
session-specific instructions
- - tightened file modes for the app secrets file. it will now fail to create 
if it already exists, and modes are better enforced
- - Switch from `asyncinotify` to `watchfiles` to allow for functionality on 
other platforms, such as macOS
- - Updated quart dependency (0.19.4 -> 0.20.0)
-
-Changes in 0.1.8:
-- Improved compatibility with Hypercorn which uses a backport of ExceptionGroup
-  to function. This provides Python 3.10 compatibility.
-- Adjust Python dependency allow >= 3.10, and all later 3.x versions, rather 
than
-  just 3.10, 3.11, and 3.12.
-
-Changes in 0.1.7:
-- Fixed the PAT handler missing asyncio library import
-- auth.require can now require role account as a type
-
-Changes in 0.1.6:
-- Custom token handler can now be set for sessions obtained through bearer 
tokens
-- Session cookies are now secured by default (SameSite=Strict, HttpOnly, 
Secure=True)
diff --git a/asfquart/LICENSE b/asfquart/LICENSE
deleted file mode 100644
index 261eeb9..0000000
--- a/asfquart/LICENSE
+++ /dev/null
@@ -1,201 +0,0 @@
-                                 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
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright [yyyy] [name of copyright owner]
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
diff --git a/asfquart/README.md b/asfquart/README.md
deleted file mode 100644
index 7c6855a..0000000
--- a/asfquart/README.md
+++ /dev/null
@@ -1,73 +0,0 @@
-# asfquart - a Quart framework for the ASF
-<a href="https://pypi.org/project/asfquart";><img alt="PyPI" 
src="https://img.shields.io/pypi/v/asfquart.svg?color=blue&maxAge=600"; /></a>
-<a href="https://pypi.org/project/asfquart";><img alt="PyPI - Python Versions" 
src="https://img.shields.io/pypi/pyversions/asfquart.svg?maxAge=600"; /></a>
-<a 
href="https://github.com/apache/infrastructure-asfquart/actions/workflows/unit-tests.yml?query=branch%3Amain";><img
 alt="Unit Tests" 
src="https://github.com/apache/infrastructure-asfquart/actions/workflows/unit-tests.yml/badge.svg?branch=main";
 /></a>
-<a 
href="https://github.com/apache/infrastructure-asfquart/blob/main/LICENSE";><img 
alt="Apache License" 
src="https://img.shields.io/github/license/apache/infrastructure-asfquart"; 
/></a>
-
-This is a [Quart](https://github.com/pallets/quart/) framework for ASF web 
applications.
-
-On top of Quart, this package layers a lot of functionality, much of which is 
specific to
-the ASF and its infrastructure and preferred approaches for website 
application development.
-
-asfquart adds the following items to basic quart:
-
-* simple construction of the `APP`
-* default `config.yaml`
-* watching the .py and config for changes, to cause a restart/reload
-* watch SIGINT to halt and SIGUSR2 to restart/reload
-* template watching and rendering for EZT templates
-* URL path routing for pages and API endpoints
-* Oauth with our ASF provider for authn
-* LDAP group testing for authz
-* long-running tasks and their lifecycle management
-
-Current (known, public) users of asfquart:
-
-* [Board Agenda Tool](https://github.com/apache/infrastructure-agenda/)
-* [Infrastructure's Reporting 
Dashboard](https://github.com/apache/infrastructure-reporting-dashboard)
-* [ASF Self Serve 
Portal](https://github.com/apache/infrastructure-selfserve-portal)
-
-
-Future users of asfquart:
-
-* Apache STeVe
-* Identity management (replaces the old id.a.o)
-* Gitbox UI
-* ??
-
-## Primer
-
-See the [documentation page](docs/readme.md) for more information.
-
-~~~python
-import asfquart
-from asfquart.auth import Requirements as R
-
-def my_app():
-    # Construct the quart service. By default, the oauth gateway is enabled at 
/oauth.
-    app = asfquart.construct("my_app_service")
-
-    @app.route("/")
-    async def homepage():
-        return "Hello!"
-
-    @app.route("/secret")
-    @asfquart.auth.require(R.committer)
-    async def secret_page():
-      return "Secret stuff!"
-    
-    asfquart.APP.run(port=8000)
-
-
-if __name__ == "__main__":
-    my_app()
-
-~~~
-
-## Running unit tests for asfquart
-
-To run manually, use the following commands from the root dir of this repo:
-
-~~~shell
-poetry run pytest
-~~~
diff --git a/asfquart/docs/auth.md b/asfquart/docs/auth.md
deleted file mode 100644
index 7174cf1..0000000
--- a/asfquart/docs/auth.md
+++ /dev/null
@@ -1,68 +0,0 @@
-# Authentication, Authorization, and Access
-
-asfquart has built-in decorators for easily handling AAA (Authentication, 
Authorization, and Access) 
-for requests. These decorators have been built with the organizational 
structure of the ASF in mind, 
-and allow you to tailor access to each end-point to suit your specific 
requirements. These decorators will, 
-unless otherwise specified, automatically initiate an OAuth flow if 
unauthenticated access is attempted.
-
-At present, asfquart features the following auth requirements:
-
-- `asfquart.auth.Requirements.committer`: User must be a committer of any 
project to access
-- `asfquart.auth.Requirements.member`: User must be a member of the Foundation 
to access
-- `asfquart.auth.Requirements.chair`: User must be a chair of one or more 
projects
-- `asfquart.auth.Requirements.mfa_enabled`: User must be logged in using a 
method that requires multi-factor authentication
-
-These requirements can be passed to the `asfquart.auth.require` decorator to 
create a list of requirements 
-that must pass in order to make use of the endpoint.
-
-By default, requirements are implicitly in the `all_of` category, meaning they 
are AND'ed together.
-You can also OR requirements by using the `any_of` flag instead:
-
-~~~python
[email protected]   # Require a valid session of any kind (implicitly, 
committer)
-async def func():
-   pass
-
[email protected]({req1, req2})  # Chain two requirements for this 
endpoint, implicitly AND
-async def func():
-  pass
-
[email protected](all_of={req1, req2})  # Same but with explicit AND
[email protected](any_of={req1, req2})  # Same but with explicit OR 
instead
-
-# You can also use both, such as requiring (req1 AND req2) AND (either req3 OR 
req4)
[email protected](all_of={req1, req2}, any_of={req3,req4})
-~~~
-
-The example below shows how to cordon off specific end-points to certain 
groups of users:
-
-~~~python
-import asfquart
-from asfquart.auth import Requirements as R
-APP = asfquart.APP
- 
-# URL that requires some sort of ASF auth (oauth, ldap)
[email protected]("/foo")
[email protected]  # Bare decorator means just require a valid session
-async def view_that_requires_auth():
-   pass
- 
-# URL that requires 2FA (implies oauth since ldap doesn't have 2fa)
[email protected]("/foo2fa")
[email protected]({R.mfa_enabled})
-async def view_that_requires_2fa_auth():
-   pass
- 
-# URL that requires a certain org role (2FA implied??)
[email protected]("/foorole")
[email protected]({R.member})
-async def view_that_requires_member_role():
-   pass
-
-# URL that needs at least one of multiple requirements, using the any_of 
directive
[email protected]("/multirole")
[email protected](any_of={R.member, R.chair})  # Either chair or member 
(or both) required
-async def view_that_requires_some_role():
-   pass
-
-~~~
diff --git a/asfquart/docs/oauth.md b/asfquart/docs/oauth.md
deleted file mode 100644
index 884cc51..0000000
--- a/asfquart/docs/oauth.md
+++ /dev/null
@@ -1,21 +0,0 @@
-# OAuth workflows
-
-asfquart will, by default, set up an OAuth endpoint at `/auth`. 
Unauthenticated access 
-to any restricted end-point will automatically trigger a redirect to the OAuth 
workflow 
-and redirect back to the restricted end-point once successful. 
-
-You can tailor these automatic behavior to suit your need, as shown in this 
example:
-
-~~~python
-import asfquart
-
-# Construct an app, with auto OAuth at /my_oauth
-app = asfquart.construct("myapp", oauth="/my_oauth")
-
-# Make another app, but do not enable oauth nor force login redirect (implied 
by no oauth)
-otherapp = asfquart.construct("otherapp", oauth=None)
-
-# Make a third app, enable oauth at /auth, but do not force logins
-thirdapp = asfquart.construct("thirdapp", oauth="/auth", force_login=False)
-~~~
-
diff --git a/asfquart/docs/readme.md b/asfquart/docs/readme.md
deleted file mode 100644
index 3f771df..0000000
--- a/asfquart/docs/readme.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# Bootstrapping asfquart
-
-## Constructing the base quart app
-
-~~~python
-import asfquart
-
-def main():
-    app = asfquart.construct("name_of_app")
-    return app
-
-if __name__ == "__main__":
-    app = main()
-~~~
-
-Other modules in the app will use:
-~~~
-import asfquart
-APP = asfquart.APP
-
[email protected]_decorator
-async def some_endpoint():
-    return do_something()
-~~~
-
-## See also (WIP):
-
-- [Setting up OAuth](oauth.md)
-- [Authentication, Authorization, and Access](auth.md)
-- [Simplified EZT templating](templates.md)
diff --git a/asfquart/docs/sessions.md b/asfquart/docs/sessions.md
deleted file mode 100644
index 6a41f38..0000000
--- a/asfquart/docs/sessions.md
+++ /dev/null
@@ -1,69 +0,0 @@
-# Session handling
-
-OAuth user sessions can be accessed through the `asfquart.session` component, 
and are encrypted 
-to ensure authenticity.
-
-The login and logout flow is automatically handled by asfquart, unless 
explicitly told not to, 
-and sessions can be accessed and modified as needed:
-
-~~~python
-
[email protected]  # Implicitly require a valid (non-empty) session
-async def endpoint_with_session():
-   session = await asfquart.session.read()  # Read user session dict
-   session["foobar"] = 42
-   asfquart.session.write(session)  # Store our changes in the user session
-~~~
-
-Session timeouts can be handled by passing the `expiry_time` argument to the 
`read()` call:
-
-~~~python
-session = await asfquart.session.read(expiry_time=24*3600)  # Require a 
session that has been accessed in the past 24 hours.
-assert session, "No session found or session expired"  # If too old or not 
found, read() returns None
-~~~
-
-
-## Role account management via declared PAT handler
-Role accounts (or regular users) can access asfquart apps by using a bearer 
token, so long as a personal app token (PAT) handler 
-is declared:
-
-~~~python
-async def token_handler(token):
-   if token == "abcdefg":
-      return {
-         "uid": "roleaccountthing",
-         "roleaccount": True,  # For role accounts, this MUST be set to True, 
to distinguish from normal user PATs
-         "committees": ["httpd", "tomcat",],  # random restriction in this 
example
-         "metadata": {  # the optional metadata dict can be used to manage/log 
whatever internal information you wish
-             "scope": ["scopeA", "scopeB"],  # For instance, you can log which 
scopes this token can be used within
-         },
-      }
-asfquart.APP.token_handler = token_handler
-~~~
-
-This would enable the role account to be granted a session through the bearer 
auth header:
-
-~~~bash
-curl -H "Authorization: bearer abcdefg" https://foo.apache.org/some-endpoint
-~~~
-
-## Using scopes for tokens
-If the application makes use of scopes to limit what an access token can do, 
you can note these scopes inside the 
-`metadata` dictionary when constructing the session dict to return. The 
`metadata` dict can be used for whatever 
-information you wish to keep about a specific session, and is accessed through 
`session.metadata` when fetched via 
-the usual `session = await asfquart.session.read()` call. Thus, you can test 
your tokens at the endpoint:
-
-~~~python
-REQUIRED_SCOPE = "admin"  # some random scope required for this endpoint
-
[email protected]("/foo")
[email protected]  # Require a valid session, can be through a PAT
-async def endpoint():
-    # Get session
-    session = await asfquart.session.read()
-    # Ensure it has the right scope, or bail
-    if REQUIRED_SCOPE not in session.metadata.get("scope", []):
-        raise asfquart.auth.AuthenticationFailed("Your token does not have the 
required scope for this endpoint")
-    # No bail? scope must be proper, do the things..
-    do_scoped_stuff()
-
diff --git a/asfquart/docs/templates.md b/asfquart/docs/templates.md
deleted file mode 100644
index 3ba7529..0000000
--- a/asfquart/docs/templates.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# Simplified EZT templating
-
-**asfquart** has built-in decorators to apply an EZT template to a 
route/handler
-to generate an HTML response page from a "data dictionary".
-
-EZT will take a template, and a data dictionary as inputs/value for that
-template, to produce an HTML page. The decorator specifies the template,
-and the function returns that data dictionary. These are combined and
-used as the page response for the route/endpoint.
-
-~~~python
-import asfquart
-APP = asfquart.APP
-
[email protected]_template('templates/example.ezt')
-async def page_example():
-    data = {
-        'title': 'Example page',
-        'count': 42,
-        }
-    return data
-~~~
-
-The `APP.use_template()` decorator takes an EZT Template instance, or
-a path to a source file. For the latter, it will install a "watcher" on
-that file. Should it change, the template will be automatically reloaded
-immediately. Its next rendering will use the changes, and no application
-restart is necessary.
-
-The path form of `use_template()` takes a path relative to `APP.app_dir`
-or an absolute path.
-
-## Templates shared among routes
-
-There are many times when a template might be shared across several routes.
-In such a scenario, the usage demonstrated above will load the template several
-times and the watcher will overwrite itself. Theoretically. This is as yet 
untested.
-And it should not be a problem. So they say.
-
-The preferred approach is to load the template once, register it for watching,
-and then to provide the template to the use/render decorator. Example:
-
-~~~python
-import asfquart
-APP = asfquart.APP
-
-T_EXAMPLE = APP.load_template('templates/example.ezt')
-
[email protected]_template(T_EXAMPLE)
-async def page_example():
-    data = {
-        'title': 'Example page',
-        'count': 42,
-        }
-    return data
-
[email protected]_template(T_EXAMPLE):
-async def other_example():
-    ...
-    return other_data
-~~~
diff --git a/asfquart/examples/snippets/personal_access_tokens.py 
b/asfquart/examples/snippets/personal_access_tokens.py
deleted file mode 100644
index 81a8f09..0000000
--- a/asfquart/examples/snippets/personal_access_tokens.py
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/usr/bin/env python3
-""" Example handler for personal access tokens (PATs) in asfquart """
-import os
-import yaml
-import easydict
-import asyncio
-import asfquart
-
-
-ROLE_ACCOUNT_CONFIG = "roleaccounts.yaml"  # See roleaccounts.yaml in this 
example dir
-
-def load_accounts():
-  if os.path.isfile(ROLE_ACCOUNT_CONFIG):
-    yml = easydict.EasyDict(yaml.safe_load(open(ROLE_ACCOUNT_CONFIG)))
-  else:
-    print(f"Could not find role account config file {ROLE_ACCOUNT_CONFIG}, no 
role accounts set up")
-    yml = {}
-
-async def token_handler(token):
-  # Iterate through all role accounts
-  for rolename, roledata in yml.items():
-    # If token matches, return the session dict.
-    # SHOULD have: uid, email, fullname, roleaccount
-    if roledata.token == token:
-      session = {
-        "uid": rolename,
-        "email": "[email protected]",
-        "fullname": roledata.name,
-        "roleaccount": True,
-        "metadata": {
-          "scope": roledata.scope,  # Mark the scope of this roleaccount (or 
PAT) internally
-        }
-      }
-      return session
-
-def my_app():
-    app = asfquart.construct("my_simple_app")
-    app.token_handler = token_handler  # Set the PAT handler
-
-    # Default homepage
-    @app.route("/")
-    async def homepage():
-        return "Hello!"
-
-    # Add a secret roleaccount-only URI for testing
-    @app.route("/secret")
-    @asfquart.auth.require(asfquart.auth.Requirements.roleaccount)
-    async def secret_roleaccount_page():
-        return await asfquart.session.read()
-
-    app.run(port=8000)
-  
-
-if __name__ == "__main__":
-    my_app()
diff --git a/asfquart/examples/snippets/roleaccounts.yaml 
b/asfquart/examples/snippets/roleaccounts.yaml
deleted file mode 100644
index 71dfc3b..0000000
--- a/asfquart/examples/snippets/roleaccounts.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-# Sample role account yaml for personal_access_tokens.py
-testrole:  # UID of role account
-  token: abcdefg1234567890   # The PAT that is used
-  name: Test Role Account    # Full name (description) of the role
-  scope:                     # Sample scopes to be user internally in 
determining access levels
-    - mailinglists
-    - something_else
diff --git a/asfquart/examples/snippets/simple_app.py 
b/asfquart/examples/snippets/simple_app.py
deleted file mode 100644
index 8a5999c..0000000
--- a/asfquart/examples/snippets/simple_app.py
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/usr/bin/env python3
-"""Simple ASFQuart server example - one file for everything"""
-
-import asfquart
-import asfquart.auth
-import asfquart.generics
-import asfquart.session
-
-
-def my_app():
-    # Construct the base app. The /oauth gateway is enabled by default.
-    # To disable it, use construct("my_app", oauth=False)
-    app = asfquart.construct("my_simple_app")
-
-    # Default homepage
-    @app.route("/")
-    async def homepage():
-        return "Hello!"
-
-    # the /secret URI, which shows the session data or forces a login
-    @app.route("/secret")
-    @asfquart.auth.require(asfquart.auth.Requirements.committer)
-    async def secret_page():
-        return await asfquart.session.read()
-
-    # Authentication failures redirect to login flow.
-    asfquart.generics.enforce_login(app)
-
-    app.run(port=8000)
-
-
-if __name__ == "__main__":
-    my_app()
diff --git a/asfquart/pyproject.toml b/asfquart/pyproject.toml
deleted file mode 100644
index 9f5426d..0000000
--- a/asfquart/pyproject.toml
+++ /dev/null
@@ -1,53 +0,0 @@
-[tool.poetry]
-name = "asfquart"
-version = "0.1.10"
-authors = ["ASF Infrastructure <[email protected]>"]
-license = "Apache-2.0"
-readme = "README.md"
-classifiers = [
-    "License :: OSI Approved :: Apache Software License",
-    "Programming Language :: Python :: 3",
-    "Programming Language :: Python :: 3.10",
-    "Programming Language :: Python :: 3.11",
-    "Programming Language :: Python :: 3.12",
-    "Programming Language :: Python :: 3.13",
-    "Environment :: Web Environment",
-    "Intended Audience :: Developers",
-    "Operating System :: OS Independent",
-    "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
-    "Topic :: Software Development :: Libraries :: Python Modules",
-]
-description = "ASF Quart Framework"
-repository = "https://github.com/apache/infrastructure-asfquart";
-
-[tool.poetry.dependencies]
-python = ">=3.10,<4"
-aiohttp = "^3.9.2"
-PyYAML = "^6.0.1"
-quart = "^0.20.0"
-ezt = "~1.1"
-asfpy = "~0.52"
-easydict = "~1.13"
-exceptiongroup = { version = ">=1.1.0", python = "<3.11" }
-watchfiles = "~0.24.0"
-
-[tool.poetry.group.test.dependencies]
-pytest = "7.2.0"
-pytest-cov = "^4.0.0"
-pytest-asyncio = "^0.20.3"
-pytest-mock = "^3.10.0"
-
-[build-system]
-requires = ["poetry-core"]
-build-backend = "poetry.core.masonry.api"
-
-[tool.pytest.ini_options]
-minversion = "7.2"
-testpaths  = ["tests"]
-pythonpath = ["src"]
-python_files = "*.py"
-markers = [
-    "config: Configuration parsing tests",
-    "session: Client session management tests",
-    "auth: Authentication/Authorization tests"
-]
diff --git a/asfquart/setup.cfg b/asfquart/setup.cfg
deleted file mode 100644
index a1bb328..0000000
--- a/asfquart/setup.cfg
+++ /dev/null
@@ -1,12 +0,0 @@
-[metadata]
-long_description_content_type = text/markdown
-long_description = file: README.md, CHANGELOG.md, LICENSE
-license = Apache Software License
-
-[options]
-package_dir=
-    =src
-packages=find:
-
-[options.packages.find]
-where=src
diff --git a/asfquart/src/asfquart/__init__.py 
b/asfquart/src/asfquart/__init__.py
deleted file mode 100644
index 4fd3704..0000000
--- a/asfquart/src/asfquart/__init__.py
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env python3
-
-import sys
-import types
-
-# This fix will be unnecessary once asfpy is released with:
-# https://github.com/apache/infrastructure-asfpy/commit/330b223
-# And ASFQuart is updated to use the updated asfpy
-if sys.platform == "darwin":
-    sys.modules["asyncinotify"] = types.ModuleType("asyncinotify")
-    sys.modules["asyncinotify"]._ffi = types.ModuleType("_ffi")
-    sys.modules["asyncinotify"].Inotify = type("Inotify", (), {"__init__": 
lambda *_: None})
-
-# ensure all submodules are loaded
-from . import config, base, session, utils
-
-# This will be rewritten once construct() is called.
-APP = None
-
-# Lift the construction from base to the package level.
-construct = base.construct
diff --git a/asfquart/src/asfquart/auth.py b/asfquart/src/asfquart/auth.py
deleted file mode 100644
index 400dde8..0000000
--- a/asfquart/src/asfquart/auth.py
+++ /dev/null
@@ -1,170 +0,0 @@
-#!/usr/bin/env python3
-"""ASFQuart - Authentication methods and decorators"""
-from . import base, session
-import functools
-import typing
-import asyncio
-import collections.abc
-
-class Requirements:
-    """Various pre-defined access requirements"""
-
-    # Error messages related to tests
-    E_NOT_LOGGED_IN = "You need to be logged in to access this endpoint."
-    E_NOT_MEMBER = "This endpoint is only accessible to foundation members."
-    E_NOT_CHAIR = "This endpoint is only accessible to project chairs."
-    E_NO_MFA = "This endpoint requires you to log on using multi-factor 
authentication."
-    E_NOT_ROOT = "This endpoint is only accessible to foundation staff."
-    E_NOT_PMC = "This endpoint is only accessible to members of the foundation 
committees."
-    E_NOT_ROLEACCOUNT = "This endpoint is only accessible to role accounts."
-    
-
-    @classmethod
-    def mfa_enabled(cls, client_session: session.ClientSession):
-        """Tests for MFA enabled in the client session"""
-        return isinstance(client_session, session.ClientSession) and 
client_session.mfa is True, cls.E_NO_MFA
-
-    @classmethod
-    def committer(cls, client_session: session.ClientSession):
-        """Tests for whether the user is a committer on any project"""
-        return isinstance(client_session, session.ClientSession), 
cls.E_NOT_LOGGED_IN
-
-    @classmethod
-    def member(cls, client_session: session.ClientSession):
-        """Tests for whether the user is a foundation member"""
-        # Anything but True will cause a failure.
-        return client_session.isMember is True, cls.E_NOT_MEMBER
-
-    @classmethod
-    def chair(cls, client_session: session.ClientSession):
-        """tests for whether the user is a chair of any top-level project"""
-        # Anything but True will cause a failure.
-        return client_session.isChair is True, cls.E_NOT_CHAIR
-
-    @classmethod
-    def root(cls, client_session: session.ClientSession):
-        """tests for whether the user is a member of infra-root"""
-        # Anything but True will cause a failure.
-        return client_session.isRoot is True, cls.E_NOT_ROOT
-
-    @classmethod
-    def pmc_member(cls, client_session: session.ClientSession):
-        """tests for whether the user is a PMC member of any top-level 
project"""
-        # Anything but True will cause a failure.
-        return bool(client_session.committees), cls.E_NOT_PMC    
-
-    @classmethod
-    def roleacct(cls, client_session: session.ClientSession):
-        """tests for whether the user is a service account"""
-        # Anything but True will cause a failure.
-        return client_session.isRole is True, cls.E_NOT_ROLEACCOUNT
-
-class AuthenticationFailed(base.ASFQuartException):
-    def __init__(self, message: str = "Authentication failed", errorcode: int 
= 403):
-        self.message = message
-        self.errorcode = errorcode
-        super().__init__(self.message, self.errorcode)
-
-
-def requirements_to_iter(args: typing.Any):
-    """Converts any auth req args (single arg, list, tuple) to an iterable if 
not already one"""
-    # No args? return empty list
-    if args is None:
-        return []
-    # Single arg? Convert to list first
-    if not isinstance(args, collections.abc.Iterable):
-        args = [args]
-    # Test that each requirement is an allowed one (belongs to the 
Requirements class)
-    for req in args:
-        if not callable(req) or req != getattr(Requirements, req.__name__, 
None):
-            raise TypeError(
-                f"Authentication requirement {req} is not valid. Must belong 
to the asfquart.auth.Requirements class."
-            )
-    return args
-
-
-def require(
-    func: typing.Optional[typing.Callable] = None,
-    all_of: typing.Optional[typing.Iterable] = None,
-    any_of: typing.Optional[typing.Iterable] = None,
-):
-    """Adds authentication/authorization requirements to an endpoint. Can be a 
single requirement or a list
-    of requirements. By default, all requirements must be satisfied, though 
this can be made optional by
-    explicitly using the `all_of` or `any_of` keywords to specify optionality. 
Requirements must be part
-    of the asfquart.auth.Requirements class, which consists of the following 
test:
-
-    - mfa_enabled: The client must authenticate with a method that has MFA 
enabled
-    - committer: The client must be a committer
-    - member: The client must be a foundation member
-    - chair: The client must be a chair of a project
-
-    In addition, any endpoint decorated with @require will implicitly require 
ANY form of
-    authenticated session. This is mandatory and also works as a bare 
decorator.
-
-    Examples:
-        @require(Requirements.member)  # Require session, require ASF member
-        @require  # Require any authed session
-        @require({Requirements.mfa_enabled, Requirements.chair})  # Require 
any project chair with MFA-enabled session
-        @require(all_of=Requirements.mfa_enabled, any_of={Requirements.member, 
Requirements.chair})
-          # Require either ASF member OR project chair, but also require MFA 
enabled in any case.
-    """
-
-    async def require_wrapper(func: typing.Callable, all_of=None, any_of=None, 
*args, **kwargs):
-        client_session = await session.read()
-        errors_list = []
-        # First off, test if we have a session at all.
-        if not isinstance(client_session, dict):
-            raise AuthenticationFailed(Requirements.E_NOT_LOGGED_IN)
-
-        # Test all_of
-        all_of_set = requirements_to_iter(all_of)
-        for requirement in all_of_set:
-            passes, desc = requirement(client_session)
-            if not passes:
-                errors_list.append(desc)
-        # If we encountered an error, bail early
-        if errors_list:
-            raise AuthenticationFailed("\n".join(errors_list))
-
-        # So far, so good? Run the any_of if present, break if any single test 
succeeds.
-        any_of_set = requirements_to_iter(any_of)
-        for requirement in any_of_set:
-            passes, desc = requirement(client_session)
-            if not passes:
-                errors_list.append(desc)
-            else:
-                # If a test passed, we can clear the failures and pass
-                errors_list.clear()
-                break
-        # If no tests passed, errors_list should have at least one entry.
-        if errors_list:
-            raise AuthenticationFailed("\n".join(errors_list))
-        if args or kwargs:
-            return await func(*args, **kwargs)
-        return await func()
-
-    # If decorator is passed without arguments, func will be an async function
-    # In this case, we will return a simple wrapper.
-    if asyncio.iscoroutinefunction(func):
-        return functools.wraps(func)(functools.partial(require_wrapper, func))
-
-    # If passed with args, we construct a "double wrapper" and return it.
-    def require_with_args(original_func: typing.Callable):
-        # If decorated without keywords, func disappears in the outer scope 
and is replaced with all_of,
-        # so we account for this by swapping around the arguments just in time 
if needed.
-        if not asyncio.iscoroutinefunction(func):
-            return functools.wraps(original_func)(
-                functools.partial(
-                    require_wrapper,
-                    original_func,
-                    all_of=requirements_to_iter(all_of or func),
-                    any_of=requirements_to_iter(any_of),
-                )
-            )
-        return functools.wraps(original_func)(
-            functools.partial(
-                require_wrapper, original_func, 
all_of=requirements_to_iter(all_of), any_of=requirements_to_iter(any_of)
-            )
-        )
-
-    return require_with_args
diff --git a/asfquart/src/asfquart/base.py b/asfquart/src/asfquart/base.py
deleted file mode 100644
index 7f8d1aa..0000000
--- a/asfquart/src/asfquart/base.py
+++ /dev/null
@@ -1,389 +0,0 @@
-#!/usr/bin/env python3
-
-"""ASFQuart - Base application/event-loop module.
-
-USAGE:
-
-  main.py:
-    import asfquart
-    APP = asfquart.construct('selfserve')
-
-  anywhere else:
-    import asfquart
-    APP = asfquart.APP
-
-
-Quart.app defines a "name" property which can be used as an APP "ID"
-(eg. discriminator for cookies). While most Quart apps use the module
-name for this (and internally Quart calls this .import_name), it can
-be anything and the .name property treats it as arbitrary.
-"""
-
-import sys
-import asyncio
-import pathlib
-import secrets
-import os
-import stat
-import logging
-import signal
-
-import asfpy.twatcher
-import quart  # implies .app and .utils
-import hypercorn.utils
-import ezt
-import easydict
-import yaml
-import watchfiles
-
-import __main__
-from . import utils
-
-try:
-    ExceptionGroup
-except NameError:
-    # This version does not have an ExceptionGroup (introduced in
-    # Python 3.11). Somebody (hypercorn) might be using a backport
-    # of it from the "exceptiongroup" package. We'll catch that
-    # instead. Note that packages designed for less than 3.11
-    # won't be throwing ExceptionGroup (of any form) at all, which
-    # means our catching it will be a no-op.
-    if sys.version_info < (3, 11):
-        from exceptiongroup import ExceptionGroup
-
-LOGGER = logging.getLogger(__name__)
-SECRETS_FILE_MODE = stat.S_IRUSR | stat.S_IWUSR  # 0o600, read/write for this 
user only
-SECRETS_FILE_UMASK = 0o777 ^ SECRETS_FILE_MODE  # Prevents existing umask from 
mangling the mode
-CONFIG_FNAME = 'config.yaml'
-
-
-class ASFQuartException(Exception):
-    """Global ASFQuart exception with a message and an error code, for the 
HTTP response."""
-
-    def __init__(self, message: str = "An error occurred", errorcode: int = 
500):
-        self.message = message
-        self.errorcode = errorcode
-        super().__init__(self.message)
-
-
-class QuartApp(quart.Quart):
-    """Subclass of quart.Quart to include our specific features."""
-
-    def __init__(self, app_id, *args, **kw):
-        super().__init__(app_id, *args, **kw)
-
-        # Locate the app dir as best we can. This is used for app ID
-        # and token filepath generation
-        # TODO: hypercorn does not have a __file__ variable available,
-        # so we are forced to fall back to CWD. Maybe have an optional arg
-        # for setting the app dir?
-        if hasattr(__main__, "__file__"):
-            self.app_dir = pathlib.Path(__main__.__file__).parent
-        else:  # No __file__, probably hypercorn, fall back to cwd for now
-            self.app_dir = pathlib.Path(os.getcwd())
-        self.app_id = app_id
-
-        # check if a path to a config file is given, otherwise default to 
CONFIG_FNAME
-        self.cfg_path = self.app_dir / kw.pop("cfg_path", CONFIG_FNAME)
-
-        # Most apps will require a watcher for their EZT templates.
-        self.tw = asfpy.twatcher.TemplateWatcher()
-        self.add_runner(self.tw.watch_forever, name=f"TW:{app_id}")
-
-        # use an easydict for config values
-        self.cfg = easydict.EasyDict()
-
-        # token handler callback for PATs - see docs/sessions.md
-        self.token_handler = None  # Default to no PAT handler available.
-
-        # Read, or set and write, the application secret token for
-        # session encryption. We prefer permanence for the session
-        # encryption, but will fall back to a new secret if we
-        # cannot write a permanent token to disk...with a warning!
-        _token_filename = self.app_dir / "apptoken.txt"
-
-        if os.path.isfile(_token_filename):  # Token file exists, try to read 
it
-            # Test that permissions are as we want them, warn if not, but 
continue
-            st = os.stat(_token_filename)
-            file_mode = st.st_mode & 0o777
-            if file_mode != SECRETS_FILE_MODE:
-                sys.stderr.write(
-                    f"WARNING: Secrets file {_token_filename} has file mode 
{oct(file_mode)}, we were expecting {oct(SECRETS_FILE_MODE)}\n"
-                )
-            self.secret_key = open(_token_filename).read()
-        else:  # No token file yet, try to write, warn if we cannot
-            self.secret_key = secrets.token_hex()
-            ### TBD: throw the PermissionError once we stabilize how to locate
-            ### the APP directory (which can be thrown off during testing)
-            try:
-                # New secrets files should be created with chmod 600, to 
ensure that only
-                # the app has access to them. umask is recorded and changed 
during this, to 
-                # ensure we don't have umask overriding what we want to 
achieve.
-                umask_original = os.umask(SECRETS_FILE_UMASK)  # Set new 
umask, log the old one
-                try:
-                    fd = os.open(_token_filename, flags=(os.O_WRONLY | 
os.O_CREAT | os.O_EXCL), mode=SECRETS_FILE_MODE)
-                finally:
-                    os.umask(umask_original)  # reset umask to the original 
setting
-                with open(fd, "w") as sfile:
-                    sfile.write(self.secret_key)
-            except PermissionError:
-                LOGGER.error(f"Could not open {_token_filename} for writing. 
Session permanence cannot be guaranteed!")
-
-    def runx(self, /,
-             host="0.0.0.0", port=None,
-             debug=True, loop=None,
-             certfile=None, keyfile=None,
-             extra_files=frozenset(), # OK, because immutable
-             ):
-        """Extended version of Quart.run()
-
-        LOOP is the loop this app should run within. One will be constructed,
-        if this is not provided.
-
-        EXTRA_FILES is a set of files (### relative to?) that should be
-        watched for changes. If a change occurs, the app will be reloaded.
-        """
-
-        # Default PORT is None, but it must be explicitly specified.
-        assert port, "The port must be specified."
-
-        # NOTE: much of the code below is direct from quart/app.py:Quart.run()
-        # This local "copy" is to deal with the custom watcher/reloader.
-
-        if loop is None:
-            loop = asyncio.new_event_loop()
-            loop.set_debug(debug)
-
-            asyncio.set_event_loop(loop)
-
-        # Create a factory for a trigger that watches for exceptions.
-        trigger = self.factory_trigger(loop, extra_files)
-
-        # Construct a task to run the app.
-        task = self.run_task(
-            host,
-            port,
-            debug,
-            certfile=certfile,
-            keyfile=keyfile,
-            shutdown_trigger=trigger,
-        )
-
-        ### LOG/print some info about the app starting?
-        print(f' * Serving Quart app "{self.app_id}"')
-        print(f" * Debug mode: {self.debug}")
-        print(" * Using reloader: CUSTOM")
-        print(f" * Running on http://{host}:{port}";)
-        print(" * ... CTRL + C to quit")
-
-        # Ready! Start running the app.
-        self.run_forever(loop, task)
-        # Being here, means graceful exit.
-
-    def factory_trigger(self, loop, extra_files=frozenset()):
-        """Factory for an AWAITABLE that handles special exceptions.
-
-        The LOOP normally ignores all signals. This method will make the
-        loop catch SIGTERM/SIGINT, then set an Event to raise an exception
-        for a clean exit.
-
-        This will also observe files for changes, and signal the loop
-        to reload the application.
-        """
-
-        # Note: Quart.run() allows for optional signal handlers. We do not.
-
-        shutdown_event = asyncio.Event()
-        def _shutdown_handler(*_) -> None:
-            shutdown_event.set()
-        loop.add_signal_handler(signal.SIGTERM, _shutdown_handler)
-        loop.add_signal_handler(signal.SIGINT, _shutdown_handler)
-        async def shutdown_wait():
-            "Log a nice message when we're signalled to shut down."
-            await shutdown_event.wait()
-            LOGGER.info('SHUTDOWN: Performing graceful exit...')
-            gathered.cancel()
-            raise hypercorn.utils.ShutdownError()
-
-        restart_event = asyncio.Event()
-        def _restart_handler(*_) -> None:
-            restart_event.set()
-        loop.add_signal_handler(signal.SIGUSR2, _restart_handler)
-        async def restart_wait():
-            "Log a nice message when we're signalled to restart."
-            await restart_event.wait()
-            LOGGER.info('RESTART: Performing process restart...')
-            gathered.cancel()
-            raise quart.utils.MustReloadError()
-
-        # Normally, for the SHUTDOWN_TRIGGER, it simply completes and
-        # returns (eg. waiting on an event) as it gets wrapped into
-        # hypercorn.utils.raise_shutdown() to raise ShutdownError.
-        #
-        # We are gathering three tasks, each running forever until its
-        # condition raises an exception.
-        #
-        # .watch() will raise MustReloadError
-        # shutdown_wait() will raise ShutdownError
-        # restart_wait() will raise MustReloadError
-        t1 = loop.create_task(self.watch(extra_files),
-                              name=f'Watch:{self.app_id}')
-        t2 = loop.create_task(shutdown_wait(),
-                              name=f'Shutdown:{self.app_id}')
-        t3 = loop.create_task(restart_wait(),
-                              name=f'Restart:{self.app_id}')
-        aw = asyncio.gather(t1, t2, t3)
-
-        gathered = utils.CancellableTask(aw, loop=loop,
-                                         name=f'Trigger:{self.app_id}')
-        async def await_gathered():
-            await gathered.task
-
-        return await_gathered  # factory to create an awaitable (coro)
-
-    async def watch(self, extra_files=frozenset()):
-        "Watch all known .py files, plus some extra files (eg. configs)."
-
-        py_files = set(getattr(m, "__file__", None) for m in 
sys.modules.values())
-        py_files.remove(None)  # the built-in modules
-
-        if os.path.isfile(self.cfg_path):
-            cfg_files = { self.cfg_path }
-        else:
-            cfg_files = set()
-
-        watched_files = py_files | cfg_files | extra_files
-
-        # quiet down the watchfiles logger
-        logging.getLogger('watchfiles.main').setLevel(logging.INFO)
-
-        async for changes in watchfiles.awatch(*watched_files):
-            for event in changes:
-                if (event[0] == watchfiles.Change.modified or event[0] == 
watchfiles.Change.deleted or event[0] == watchfiles.Change.added):
-                    LOGGER.info(f"File changed: {event[1]}")
-                    raise quart.utils.MustReloadError
-        # NOTREACHED
-
-    def run_forever(self, loop, task):
-        "Run the application until exit, then cleanly shut down."
-
-        # Note: this logic is copied from quart/app.py
-        reload_ = False
-        try:
-            loop.run_until_complete(task)
-        except quart.utils.MustReloadError:
-            reload_ = True
-            LOGGER.debug('FOUND: MustReloadError')
-        except ExceptionGroup as e:
-            reload_ = (e.subgroup(quart.utils.MustReloadError) is not None)
-            LOGGER.debug(f'FOUND: ExceptionGroup, reload_={reload_}')
-        finally:
-            try:
-                quart.app._cancel_all_tasks(loop) # pylint: 
disable=protected-access
-                loop.run_until_complete(loop.shutdown_asyncgens())
-            finally:
-                asyncio.set_event_loop(None)
-                loop.close()
-        if reload_:
-            quart.utils.restart()
-
-    def load_template(self, tpath, base_format=ezt.FORMAT_HTML):
-        # Use str() to avoid passing Path instances.
-        return self.tw.load_template(str(self.app_dir / tpath), 
base_format=base_format)
-
-    def use_template(self, path_or_T, base_format=ezt.FORMAT_HTML):
-        # Decorator to use a template, specified by path or provided.
-
-        if isinstance(path_or_T, ezt.Template):
-            return utils.use_template(path_or_T)
-
-        return utils.use_template(self.load_template(path_or_T, base_format))
-
-    def add_runner(self, func, name=None):
-        "Add a long-running task, with cancellation/cleanup."
-
-        # NOTES:
-        #
-        # We take advantage of the WHILE_SERVING mechanism that uses a
-        # generator to manage the lifecycle of a task. We create/schedule
-        # a task when the app starts up, then yield back to the framework.
-        # Control returns when the app is shutting down, and we can cleanly
-        # cancel the long-running task.
-        #
-        # Contrast this with APP.background_tasks. Each task placed into
-        # that set must monitor APP.shutdown_event to know when the task
-        # should exit (or an external mechanism observing that event must
-        # cancel the task). The coordination becomes more difficult, and
-        # must be handled by the application logic. The WHILE_SERVING
-        # mechanism used here places no demands upon the caller to manage
-        # the lifecycle of the long-running task.
-        #
-        # Further note: should a task be placed into APP.background_tasks,
-        # it will be waited on to exit at shutdown time. If the task is
-        # not watching APP.shutdown_event, and does not complete, finish,
-        # or cancel within a timeout period (default is 5 seconds), then
-        # that background task is canceled. That is an unstructured
-        # completion/cancellation mechanism and introduces a delay during
-        # the shutdown process.
-
-        @self.while_serving
-        async def perform_runner():
-            ctask = utils.CancellableTask(func(), name=name)
-            #print('RUNNER STARTED:', ctask.task)
-
-            yield  # back to serving
-
-            #print('RUNNER STOPPING:', ctask.task)
-            ctask.cancel()
-
-
-def construct(name, *args, **kw):
-    ### add/alter/update ARGS and KW for our specific preferences
-
-    # By default, we will set up OAuth and force login redirect on auth failure
-    # This can be turned off by setting oauth=False in the construct call.
-    # To use a different oauth URI than the default /auth, specify the URI
-    # in the oauth argument, for instance: asfquart.construct("myapp", 
oauth="/session")
-    # Pop the arguments from KW, as the parent class doesn't understand them.
-    setup_oauth = kw.pop("oauth", True)
-    # Note: order is important, as we want the .pop() to always execute.
-    force_auth_redirect = kw.pop("force_login", True) and setup_oauth
-
-    app = QuartApp(name, *args, **kw)
-
-    @app.errorhandler(ASFQuartException)  # ASFQuart exception handler
-    async def handle_exception(error):
-        # If an error is thrown before the request body has been consumed, eat 
it quietly.
-        if not quart.request.body._complete.is_set():  # pylint: 
disable=protected-access
-            async for _data in quart.request.body:
-                pass
-        return quart.Response(status=error.errorcode, response=error.message)
-
-    # try to load the config information from app.cfg_path
-    if os.path.isfile(app.cfg_path):
-        app.cfg.update(yaml.safe_load(open(app.cfg_path)))
-
-    # Provide our standard filename argument converter.
-    import asfquart.utils
-
-    # Sane defaults for cookies: SameSite=Strict; Secure; HttpOnly
-    app.config["SESSION_COOKIE_SAMESITE"] = "Strict"
-    app.config["SESSION_COOKIE_SECURE"] = True
-    app.config["SESSION_COOKIE_HTTPONLY"] = True
-
-    app.url_map.converters["filename"] = asfquart.utils.FilenameConverter
-
-    # Set up oauth and login redirects if needed
-    if setup_oauth:
-        import asfquart.generics
-
-        # Figure out the OAuth URI we want to use.
-        oauth_uri = setup_oauth if isinstance(setup_oauth, str) else 
asfquart.generics.DEFAULT_OAUTH_URI
-        asfquart.generics.setup_oauth(app, uri=oauth_uri)
-        if force_auth_redirect:
-            asfquart.generics.enforce_login(app, redirect_uri=oauth_uri)
-
-    # Now stash this into the package module, for later pick-up.
-    asfquart.APP = app
-
-    return app
diff --git a/asfquart/src/asfquart/config.py b/asfquart/src/asfquart/config.py
deleted file mode 100644
index cf978e5..0000000
--- a/asfquart/src/asfquart/config.py
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/usr/bin/env python3
-
-"""ASFQuart - Configuration readers"""
-
-import yaml
-import functools
-import inspect
-
-DEFAULT_CONFIG_FILENAME = "config.yaml"
-
-
-async def _read_config(callback, config_filename):
-    """Reads a YAML configuration and passes it to the callback"""
-    with open(config_filename, encoding='utf-8') as r:
-        config_as_dict = yaml.safe_load(r)
-    # Some configuration routines may require os to block while the 
configuration is applied, so
-    # we will accept both sync and async callbacks.
-    # If the callback is async, await it...
-    if inspect.iscoroutinefunction(callback):
-        await callback(config_as_dict)
-    # Otherwise, just run it in blocking mode
-    else:
-        callback(config_as_dict)
-
-
-def static(func):
-    """Standard wrapper for a configuration parser. Reads config.yaml and 
passes it to the callback as a dict"""
-
-    @functools.wraps(func)
-    async def config_wrapper(config_filename=DEFAULT_CONFIG_FILENAME):
-        await _read_config(func, config_filename)
-
-    return config_wrapper
diff --git a/asfquart/src/asfquart/generics.py 
b/asfquart/src/asfquart/generics.py
deleted file mode 100644
index 2e51c0f..0000000
--- a/asfquart/src/asfquart/generics.py
+++ /dev/null
@@ -1,129 +0,0 @@
-#!/usr/bin/env python3
-"""Generic endpoints for ASFQuart"""
-
-import secrets
-import urllib
-import time
-
-import quart
-import aiohttp
-
-import asfquart  # implies .session
-
-
-# These are the ASF OAuth URLs for init and verification. Used for 
setup_oauth()
-OAUTH_URL_INIT = "https://oauth.apache.org/auth-oidc?state=%s&redirect_uri=%s";
-OAUTH_URL_CALLBACK = "https://oauth.apache.org/token-oidc?code=%s";
-DEFAULT_OAUTH_URI = "/auth"
-
-
-def setup_oauth(app, uri=DEFAULT_OAUTH_URI, workflow_timeout: int = 900):
-    """Sets up a generic ASF OAuth endpoint for the given app. The default URI 
is /auth, and the
-    default workflow timeout is 900 seconds (15 min), within which the OAuth 
login must
-    be completed. The OAuth endpoint handles everything related to logging in 
and out via OAuth,
-    and has the following actions:
-
-    - /auth?login  - Initializes an OAuth login
-    - /auth?login=/foo - Same as above, but redirects to /foo on successful 
login
-    - /auth?logout - Clears a user session, logging them out
-    - /auth  - Shows the user's credentials if logged in, 404 otherwise.
-
-    This generic route expects the Host: header of the request to be accurate, 
which means setting
-    "ProxyPreserveHost On" in your httpd config if proxying.
-    """
-
-    pending_states = {}  # keeps track of pending states and their expiry
-
-    @app.route(uri)
-    async def oauth_endpoint():
-        # Init oauth login
-        login_uri = quart.request.args.get("login")
-        if login_uri and login_uri.endswith("?"):
-            # for some reason I don't understand, quart keeps adding a '?' to 
the uri
-            login_uri = login_uri.removeprefix("?")
-        logout_uri = quart.request.args.get("logout")
-        if login_uri or quart.request.query_string == b"login":
-            state = secrets.token_hex(16)
-            # Save the time we initialized this state and the optional login 
redirect URI
-            pending_states[state] = [time.time(), login_uri]
-            callback_host = quart.request.host_url.replace("http://";, 
"https://";)  # Enforce HTTPS
-            callback_url = urllib.parse.urljoin(  # NOTE: the uri MUST start 
with a single forward slash!
-                callback_host,
-                f"{uri}?state={state}",
-            )
-            redirect_url = OAUTH_URL_INIT % (state, 
urllib.parse.quote(callback_url))
-            return quart.redirect(redirect_url)
-
-        # Log out
-        elif logout_uri or quart.request.query_string == b"logout":
-            asfquart.session.clear()
-            if logout_uri:  # if called with /auth=logout=/foo, redirect to 
/foo
-                return quart.redirect(logout_uri)
-            return quart.Response(
-                status=200,
-                response=f"Client session removed, goodbye!\n",
-            )
-        else:
-            code = quart.request.args.get("code")
-            state = quart.request.args.get("state")
-            if code and state:  # Callback from oauth, complete flow.
-                if state not in pending_states or pending_states[state][0] < 
(time.time() - workflow_timeout):
-                    pending_states.pop(state, None)  # safe pop
-                    return quart.Response(
-                        status=403,
-                        response=f"Invalid or expired OAuth state provided. 
OAuth workflows must be completed within {workflow_timeout} seconds.\n",
-                    )
-                redirect_uri = pending_states[state][1]
-                pending_states.pop(
-                    state
-                )  # Pop the state from pending. We do this straight away to 
avoid timing attacks
-                ct = aiohttp.client.ClientTimeout(sock_read=15)
-                async with aiohttp.client.ClientSession(timeout=ct) as session:
-                    rv = await session.get(OAUTH_URL_CALLBACK % code)
-                    assert rv.status == 200, "Could not verify oauth response."
-                    oauth_data = await rv.json()
-                    asfquart.session.write(oauth_data)
-                if redirect_uri:  # if called with /auth=login=/foo, redirect 
to /foo
-                    # If SameSite is set, we cannot redirect with a 30x 
response, as that may invalidate the set-cookie
-                    # instead, we issue a 200 Okay with a Refresh header, 
instructing the browser to immediately go
-                    # someplace else. This counts as a samesite request.
-                    return quart.Response(
-                        status=200,
-                        response=f"Successfully logged in! Welcome, 
{oauth_data['uid']}\n",
-                        headers={"Refresh": f"0; url={redirect_uri}"},
-                    )
-                # Otherwise, just say hi
-                return quart.Response(
-                    status=200,
-                    response=f"""
-                    Successfully signed in! Welcome, {oauth_data["uid"]}
-                    """,
-                )
-            else:  # Just spit out existing session if it's there
-                client_session = await asfquart.session.read()
-                if isinstance(client_session, asfquart.session.ClientSession):
-                    return client_session
-                return quart.Response(
-                    status=404,
-                    response=f"No active session found.\n",
-                )
-
-
-def enforce_login(app, redirect_uri=DEFAULT_OAUTH_URI):
-    """Enforces redirect to the auth provider (if enabled) when a client tries 
to access a restricted page
-    without being logged in. Only redirects if there is no active user 
session. On success, the client
-    is redirected back to the origin page that was restricted. If it is still 
restricted, the client
-    will instead see an error message."""
-    import asfquart.auth
-
-    @app.errorhandler(asfquart.auth.AuthenticationFailed)
-    async def auth_redirect(error):
-        # If we have no client session (and X-No-Redirect is not set), 
redirect to auth flow
-        if (
-            "x-no-redirect" not in quart.request.headers
-            and not quart.request.authorization
-            and not await asfquart.session.read()
-        ):
-            return 
quart.redirect(f"{redirect_uri}?login={quart.request.full_path}")
-        # If we have a session, but still no access, just say so in plain text.
-        return quart.Response(status=error.errorcode, response=error.message)
diff --git a/asfquart/src/asfquart/ldap.py b/asfquart/src/asfquart/ldap.py
deleted file mode 100644
index d676693..0000000
--- a/asfquart/src/asfquart/ldap.py
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/env python3
-"""ASFQuart - LDAP Authentication methods and decorators"""
-from . import base
-import re
-import time
-
-DEFAULT_LDAP_URI = "ldaps://ldap-eu.apache.org:636"
-DEFAULT_LDAP_BASE = "uid=%s,ou=people,dc=apache,dc=org"
-DEFAULT_LDAP_GROUP_BASE = "ou=project,ou=groups,dc=apache,dc=org"
-UID_RE = re.compile(r"^(?:uid=)?([^,]+)")
-GROUP_RE = re.compile(r"^(?:cn=)?([^,]+)")
-DEFAULT_MEMBER_ATTR = "member"
-DEFAULT_OWNER_ATTR = "owner"
-DEFAULT_LDAP_CACHE_TTL = 3600  # Cache LDAP lookups for one hour
-
-# Test if LDAP is enabled for this quart app, and if so, enable LDAP Auth 
support
-# This assumes the quart app was installed with asfpy[aioldap] in the Pipfile.
-try:
-    import asfpy.aioldap
-    import bonsai.errors
-    LDAP_SUPPORTED = True
-except ModuleNotFoundError:
-    LDAP_SUPPORTED = False
-
-LDAP_CACHE: dict[str, list] = {}  # Temporary one-hour cache to speed up 
lookups.
-
-
-class LDAPClient:
-    def __init__(self, username: str, password: str):
-        self.userid = username
-        self.dn = DEFAULT_LDAP_BASE % username
-        self.client = asfpy.aioldap.LDAPClient(DEFAULT_LDAP_URI, self.dn, 
password)
-
-    async def get_affiliations(self):
-        """Scans for which projects this user is a part of. Returns a dict 
with memberships of each
-        pmc/committer role (member/owner in LDAP)"""
-        all_projects = DEFAULT_LDAP_GROUP_BASE
-        attrs = [DEFAULT_MEMBER_ATTR, DEFAULT_OWNER_ATTR]
-        # Check LDAP cache. If found, we only need to test LDAP auth
-        try:
-            if self.userid in LDAP_CACHE and LDAP_CACHE[self.userid][0] > 
(time.time() - DEFAULT_LDAP_CACHE_TTL):
-                async with self.client.connect():
-                    pass
-            else:
-                ldap_groups = {attr: [] for attr in attrs}
-                async with self.client.connect() as conn:
-                    rv = await conn.search(all_projects, attrs)
-                    if not rv:
-                        raise Exception("Empty result set returned by LDAP")  
# pylint: disable=broad-exception-raised
-                    for project in rv:
-                        if "dn" in project and any(xattr in project for xattr 
in attrs):
-                            dn_match = GROUP_RE.match(str(project["dn"]))
-                            if dn_match:
-                                project_name = dn_match.group(1)
-                                for xattr in attrs:
-                                    if self.dn in project.get(xattr, []):
-                                        ldap_groups[xattr].append(project_name)
-                    LDAP_CACHE[self.userid] = (time.time(), ldap_groups)
-            return LDAP_CACHE[self.userid][1]
-
-        except bonsai.errors.AuthenticationError as e:
-            raise base.ASFQuartException(f"Invalid credentials provided: {e}", 
errorcode=403)
-        except Exception as e:
-            print(f"Base exception during LDAP lookup: {e}")
-            raise base.ASFQuartException(
-                "Could not perform LDAP authorization check, please try again 
later.", errorcode=500
-            )
diff --git a/asfquart/src/asfquart/session.py b/asfquart/src/asfquart/session.py
deleted file mode 100644
index 795c802..0000000
--- a/asfquart/src/asfquart/session.py
+++ /dev/null
@@ -1,119 +0,0 @@
-#!/usr/bin/env python3
-"""ASFQuart - User session methods and decorators"""
-import typing
-
-from . import base, ldap
-import time
-import binascii
-
-import quart.sessions
-import asfquart
-import asyncio
-
-
-class ClientSession(dict):
-    def __init__(self, raw_data: dict):
-        """Initializes a client session from a raw dict. ClientSession is a 
subclassed dict, so that
-        we can send it to quart in a format it can render."""
-        super().__init__()
-        self.uid = raw_data.get("uid")
-        self.dn = raw_data.get("dn")
-        self.fullname = raw_data.get("fullname")
-        self.email = raw_data.get("email", f"{self.uid}@apache.org")
-        self.isMember = raw_data.get("isMember", False)
-        self.isChair = raw_data.get("isChair", False)
-        self.isRoot = raw_data.get("isRoot", False)
-        self.committees = raw_data.get("pmcs", [])
-        self.projects = raw_data.get("projects", [])
-        self.mfa = raw_data.get("mfa", False)
-        self.isRole = raw_data.get("roleaccount", False)
-        self.metadata = raw_data.get("metadata", {})  # This can contain 
whatever specific metadata the app needs
-        # Update the external dict representation with internal values
-        self.update(self.__dict__.items())
-
-
-async def read(expiry_time=86400*7, app=None) -> 
typing.Optional[ClientSession]:
-    """Fetches a cookie-based session if found (and valid), and updates the 
last access timestamp
-    for the session."""
-
-    if app is None:
-        app = asfquart.APP
-
-    # We store the session cookie using the app.app_id identifier, to 
distinguish between
-    # two asfquart apps running on the same hostname.
-    cookie_id = app.app_id
-    if cookie_id in quart.session:
-        now = time.time()
-        cookie_expiry_deadline = now - expiry_time
-        session_dict = quart.session[cookie_id]
-        if isinstance(session_dict, dict):
-            session_update_timestamp = session_dict.get("uts", 0)
-            # If a session cookie has expired (not updated/used for seven 
days), we delete it instead of returning it
-            if session_update_timestamp < cookie_expiry_deadline:
-                del quart.session[cookie_id]
-            # If it's still valid, use it
-            else:
-                # Update the timestamp, since the session has been requested 
(and thus used)
-                session_dict["uts"] = now
-                return ClientSession(session_dict)
-    # Check for session providers in Auth header. These sessions are created 
ad-hoc, and do not linger in the
-    # quart session DB. Since quart.request is not defined inside testing 
frameworks, the bool(request) test
-    # asks the werkzeug LocalProxy wrapper whether a request exists or not, 
and bails if not.
-    elif bool(quart.request) and 'Authorization' in quart.request.headers:
-        match quart.request.authorization.type:
-                case "bearer":  # Role accounts, PATs - TBD
-                    if app.token_handler:
-                        assert callable(app.token_handler), "app.token_handler 
is not a callable function!"
-                        session_dict = None  # Blank, in case we don't have a 
working callback.
-                        # Async token handler?
-                        if asyncio.iscoroutinefunction(app.token_handler):
-                            session_dict = await 
app.token_handler(quart.request.authorization.token)
-                        # Sync handler?
-                        elif callable(app.token_handler):
-                            session_dict = 
app.token_handler(quart.request.authorization.token)
-                        # If token handler returns a dict, we have a session 
and should set it up
-                        if session_dict:
-                            return ClientSession(session_dict)
-                    else:
-                        print(f"Debug: No PAT handler registered to handle 
token {quart.request.authorization.token}")
-                case "basic":  # Basic LDAP auth - will need to grab info from 
LDAP
-                    if ldap.LDAP_SUPPORTED:
-                        try:
-                            auth_user = 
quart.request.authorization.parameters["username"]
-                            auth_pwd = 
quart.request.authorization.parameters["password"]
-                            ldap_client = ldap.LDAPClient(auth_user, auth_pwd)
-                            ldap_affiliations = await 
ldap_client.get_affiliations()
-                            # Convert to the usual session dict. TODO: add a 
single standardized parser/class for sessions
-                            session_dict = {
-                                "uid": auth_user,
-                                "pmcs": 
ldap_affiliations[ldap.DEFAULT_OWNER_ATTR],
-                                "projects": 
ldap_affiliations[ldap.DEFAULT_MEMBER_ATTR],
-                            }
-                            return ClientSession(session_dict)
-                        except (binascii.Error, ValueError, KeyError) as e:
-                            # binascii/ValueError == bad base64 auth string
-                            # KeyError = missing username or password
-                            raise base.ASFQuartException("Invalid 
Authorization header provided", errorcode=400)
-                case default:
-                    raise base.ASFQuartException("Not implemented yet", 
errorcode=501)
-
-
-def write(session_data: dict, app=None):
-    """Sets a cookie-based user session for this app"""
-
-    if app is None:
-        app = asfquart.APP
-
-    cookie_id = app.app_id
-    dict_copy = session_data.copy()  # Copy dict so we don't mess with the 
original data
-    dict_copy["uts"] = time.time()   # Set last access timestamp for expiry 
checks later
-    quart.session[cookie_id] = dict_copy
-
-
-def clear(app=None):
-    """Clears a session"""
-
-    if app is None:
-        app = asfquart.APP
-
-    quart.session.pop(app.app_id, None)  # Safely pop the session if it's 
there.
diff --git a/asfquart/src/asfquart/utils.py b/asfquart/src/asfquart/utils.py
deleted file mode 100644
index 3ec7a5c..0000000
--- a/asfquart/src/asfquart/utils.py
+++ /dev/null
@@ -1,129 +0,0 @@
-#!/usr/bin/env python3
-"""Miscellaneous helpers for ASFQuart"""
-
-import os.path
-import io
-import functools
-import asyncio
-import logging
-
-import quart
-import werkzeug.routing
-
-LOGGER = logging.getLogger(__name__)
-
-DEFAULT_MAX_CONTENT_LENGTH = 102400
-
-
-async def formdata():
-    """Catch-all form data converter. Converts form data of any form (json, 
urlencoded, mime, etc) to a dict"""
-    form_data = dict()
-    form_data.update(quart.request.args.to_dict())  # query string args
-    xform = await quart.request.form                # POST form data
-    # Pre-parse check for form data size
-    if quart.request.content_type and any(
-            x in quart.request.content_type
-            for x in (
-                    "multipart/form-data",
-                    "application/x-www-form-urlencoded",
-                    "application/x-url-encoded",
-            )
-    ):
-        # If the content is too large for us to handle, we need to silently 
ignore every chunk, so we can return with a
-        # cleared buffer, lest bad things happen.
-        max_size = quart.current_app.config.get("MAX_CONTENT_LENGTH", 
DEFAULT_MAX_CONTENT_LENGTH)
-        if quart.request.content_length > max_size:
-            async for _data in quart.request.body:
-                pass
-            return quart.Response(
-                status=413,
-                response=f"Request content length 
({quart.request.content_length} bytes) is larger than what is permitted for 
form data ({max_size} bytes)!",
-            )
-    if xform:
-        form_data.update(xform.to_dict())
-    if quart.request.is_json:  # JSON data from a PUT?
-        xjson = await quart.request.json
-        form_data.update(xjson)
-    return form_data
-
-
-class FilenameConverter(werkzeug.routing.BaseConverter):
-    """Simple converter that splits a filename into a basename and an 
extension. Only deals with filenames, not
-    full paths. Thus, <filename> will match foo.txt, but not /foo/bar.baz"""
-
-    regex = r"^[^/.]*(\.[A-Za-z0-9]+)?$"
-    part_isolating = False
-
-    def to_python(self, filename): # pylint: disable=arguments-renamed
-        return os.path.splitext(filename) # superclass function uses 'value'
-
-
-#
-# Decorator to use a template in order to generate a webpage with some
-# provided data.
-#
-# EXAMPLE:
-#
-#   @use_template(T_MAIN)
-#   def main_page():
-#       ...
-#       data = {
-#           'title': 'Main Page',
-#           'count': 42,
-#       }
-#       return data
-#
-# The data dictionary will be provided to the EZT template for
-# rendering the page.
-#
-def use_template(template):
-
-    # The @use_template(T_MAIN) example is actually a function call
-    # to *produce* a decorator function. This is that decorator. It
-    # takes a function to wrap (FUNC), and produces a wrapping function
-    # that will be used during operation (WRAPPER).
-    def decorator(func):
-
-        # .wraps() copies name/etc from FUNC onto the wrapper function
-        # that we return.
-        @functools.wraps(func)
-        async def wrapper(*args, **kw):
-            # Get the data dictionary from the page endpoint.
-            data = await func(*args, **kw)
-
-            # Render that page, and return it to Quart.
-            return render(template, data)
-
-        return wrapper
-
-    return decorator
-
-
-def render(t, data):
-    "Simple function to render a template into a string."
-    buf = io.StringIO()
-    t.generate(buf, data)
-    return buf.getvalue()
-
-
-class CancellableTask:
-    "Wrapper for a task that does not propagate its cancellation."
-
-    def __init__(self, coro, *, loop=None, name=None):
-        "Create a task for CORO in LOOP, named NAME."
-
-        if loop is None:
-            loop = asyncio.get_event_loop()
-
-        async def absorb_cancel():
-            try:
-                await coro
-            except asyncio.CancelledError:
-                LOGGER.debug(f'TASK CANCELLED: {self.task}')
-
-        self.task = loop.create_task(absorb_cancel(), name=name)
-
-    def cancel(self):
-        "Cancel the task, and clean up its CancelledError exception."
-
-        self.task.cancel()
diff --git a/asfquart/tests/auth.py b/asfquart/tests/auth.py
deleted file mode 100644
index 9c23d90..0000000
--- a/asfquart/tests/auth.py
+++ /dev/null
@@ -1,129 +0,0 @@
-#!/usr/bin/env python3
-
-import time
-
-import pytest
-import quart
-
-import asfquart.auth
-from asfquart.auth import Requirements as R
-
-
[email protected]
[email protected]
-async def test_auth_basics():
-    app = asfquart.construct("foobar")
-
-    # Generic auth test, just requires a valid session
-    @asfquart.auth.require
-    async def requires_session():
-        pass
-
-    # Test with no session, should fail
-    quart.session = {}
-    try:
-        await requires_session()
-    except asfquart.auth.AuthenticationFailed as e:
-        assert e.message is R.E_NOT_LOGGED_IN
-
-    # Test with session, should work.
-    quart.session = {app.app_id: {"uts": time.time(), "foo": "bar"}}
-    await requires_session()
-
-    # Test with a bad requirement, should fail with a TypeError.
-    with pytest.raises(TypeError):
-        @asfquart.auth.require({R.member, print})
-        async def requires_bad_thing():
-            pass
-    # Same bad one, but with explicit any_of
-    with pytest.raises(TypeError):
-        @asfquart.auth.require(any_of={R.member, print})
-        async def requires_bad_thingy():
-            pass
-
-
[email protected]
[email protected]
-async def test_mfa_auth():
-    """MFA tests"""
-
-    app = asfquart.construct("foobar")
-
-    @asfquart.auth.require(R.mfa_enabled)
-    async def requires_mfa():
-        pass
-
-    # Test MFA with no session, should fail exactly like auth_required
-    quart.session = {}
-    try:
-        await requires_mfa()
-    except asfquart.auth.AuthenticationFailed as e:
-        assert e.message is R.E_NOT_LOGGED_IN
-
-    # Test with session without MFA, should fail.
-    quart.session = {app.app_id: {"uts": time.time(), "foo": "bar"}}
-    try:
-        await requires_mfa()
-    except asfquart.auth.AuthenticationFailed as e:
-        assert e.message is R.E_NO_MFA
-
-    # Test with session with MFA, should work.
-    quart.session = {app.app_id: {"uts": time.time(), "foo": "bar", "mfa": 
True}}
-    await requires_mfa()
-
-
[email protected]
[email protected]
-async def test_role_auth():
-    """Role tests"""
-
-    app = asfquart.construct("foobar")
-
-    # Set up some role tests
-    @asfquart.auth.require  # no args implies any valid account
-    async def test_committer_auth():
-        pass
-
-    @asfquart.auth.require(R.member)
-    async def test_member_auth():
-        pass
-
-    @asfquart.auth.require({R.member, R.chair})
-    async def test_member_and_chair_auth():
-        pass
-
-    @asfquart.auth.require(any_of={R.member, R.chair})
-    async def test_member_or_chair_auth():
-        pass
-
-    # Test role with no session, should fail exactly like auth_required
-    quart.session = {}
-    try:
-        await test_committer_auth()
-    except asfquart.auth.AuthenticationFailed as e:
-        assert e.message is R.E_NOT_LOGGED_IN
-
-    # Test with session , should work
-    quart.session = {app.app_id: {"uts": time.time(), "foo": "bar"}}
-    await test_committer_auth()
-
-    # Test with a role we don't have, should fail
-    try:
-        await test_member_auth()
-    except asfquart.auth.AuthenticationFailed as e:
-        assert e.message is R.E_NOT_MEMBER
-
-    # Test with for both member and chair, while only being member. should 
pass on member check, fail on chair
-    quart.session = {app.app_id: {"uts": time.time(), "foo": "bar", 
"isMember": True}}
-    try:
-        await test_member_and_chair_auth()
-    except asfquart.auth.AuthenticationFailed as e:
-        assert e.message is R.E_NOT_CHAIR
-
-    # Test for either member of chair, should work as we have chair (but not 
member)
-    quart.session = {app.app_id: {"uts": time.time(), "foo": "bar", "isChair": 
True}}
-    await test_member_or_chair_auth()
-
-    # Test for both member and chair, when we are both. should work.
-    quart.session = {app.app_id: {"uts": time.time(), "foo": "bar", 
"isMember": True, "isChair": True}}
-    await test_member_and_chair_auth()
diff --git a/asfquart/tests/config.py b/asfquart/tests/config.py
deleted file mode 100644
index 6bac3a1..0000000
--- a/asfquart/tests/config.py
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/usr/bin/env python3
-
-import pathlib
-
-import pytest
-import asfquart
-
-TEST_CONFIG_FILENAME = pathlib.Path(__file__).parent / "data/config.test.yaml"
-times_loaded = 0
-
-
[email protected]
[email protected]
-async def test_config_static():
-    """Tests static (one-time) configuration parsing in blocking and async 
mode"""
-
-    @asfquart.config.static
-    def config_callback(yml: dict):
-        assert yml, "Config YAML is empty!"
-        assert isinstance(yml, dict), "Config YAML is not a dict!"
-
-    # Async test
-    # the decorator @asfquart.config.static wraps the function to an async 
method
-    # suppress inspections as they fail to recognize that
-    await config_callback(TEST_CONFIG_FILENAME)  # noqa
diff --git a/asfquart/tests/data/config.test.yaml 
b/asfquart/tests/data/config.test.yaml
deleted file mode 100644
index f3951c7..0000000
--- a/asfquart/tests/data/config.test.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-foo: bar
-bar:
-  - baz
-  - blorp
-  - bleep
-number: 42
-gnomes: true
diff --git a/asfquart/tests/session.py b/asfquart/tests/session.py
deleted file mode 100644
index 848c615..0000000
--- a/asfquart/tests/session.py
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/bin/env python3
-
-import time
-
-import pytest
-import quart
-import asfquart
-
-
[email protected]
[email protected]
-async def test_sessions():
-    app = asfquart.construct("foobar")
-    quart.session = {app.app_id: {"uts": time.time(), "uid": "bar"}}
-    my_session = await asfquart.session.read()
-    assert my_session, "Was expecting a session, but got nothing in return"
-    assert my_session.uid == "bar", f"session value 'uid' should be 'bar', but 
was '{my_session.uid}'"
diff --git a/atr/blueprints/admin/__init__.py b/atr/blueprints/admin/__init__.py
index ae91fdc..6b4371c 100644
--- a/atr/blueprints/admin/__init__.py
+++ b/atr/blueprints/admin/__init__.py
@@ -19,11 +19,11 @@
 
 from typing import Final
 
-import quart
-
 import asfquart.auth as auth
 import asfquart.base as base
 import asfquart.session as session
+import quart
+
 import atr.util as util
 
 BLUEPRINT: Final = quart.Blueprint("admin", __name__, url_prefix="/admin", 
template_folder="templates")
diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index 9a439a9..e103b86 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -22,12 +22,12 @@ from collections.abc import Callable, Mapping
 from typing import Any
 
 import aiofiles.os
+import asfquart.base as base
+import asfquart.session as session
 import httpx
 import quart
 import werkzeug.wrappers.response as response
 
-import asfquart.base as base
-import asfquart.session as session
 import atr.blueprints.admin as admin
 import atr.datasources.apache as apache
 import atr.db as db
diff --git a/atr/routes/__init__.py b/atr/routes/__init__.py
index 849f78f..603fe89 100644
--- a/atr/routes/__init__.py
+++ b/atr/routes/__init__.py
@@ -25,10 +25,10 @@ from typing import Any, ParamSpec, TypeVar
 
 import aiofiles
 import aiofiles.os
+import asfquart
 import quart
 import werkzeug.datastructures as datastructures
 
-import asfquart
 import atr.db.models as models
 
 if asfquart.APP is ...:
diff --git a/atr/routes/candidate.py b/atr/routes/candidate.py
index 43b512e..53e776e 100644
--- a/atr/routes/candidate.py
+++ b/atr/routes/candidate.py
@@ -20,14 +20,14 @@
 import datetime
 import secrets
 
-import quart
-import werkzeug.wrappers.response as response
-import wtforms
-
 import asfquart
 import asfquart.auth as auth
 import asfquart.base as base
 import asfquart.session as session
+import quart
+import werkzeug.wrappers.response as response
+import wtforms
+
 import atr.db as db
 import atr.db.models as models
 import atr.routes as routes
diff --git a/atr/routes/dev.py b/atr/routes/dev.py
index 765370d..597507b 100644
--- a/atr/routes/dev.py
+++ b/atr/routes/dev.py
@@ -16,12 +16,12 @@
 # under the License.
 
 
-import quart
-
 import asfquart as asfquart
 import asfquart.auth as auth
 import asfquart.base as base
 import asfquart.session as session
+import quart
+
 import atr.db as db
 import atr.db.models as models
 import atr.routes as routes
diff --git a/atr/routes/docs.py b/atr/routes/docs.py
index 5d350a1..c4fb156 100644
--- a/atr/routes/docs.py
+++ b/atr/routes/docs.py
@@ -17,10 +17,10 @@
 
 """docs.py"""
 
-import quart
-
 import asfquart as asfquart
 import asfquart.auth as auth
+import quart
+
 import atr.routes as routes
 
 
diff --git a/atr/routes/download.py b/atr/routes/download.py
index 8496c7e..783bbfc 100644
--- a/atr/routes/download.py
+++ b/atr/routes/download.py
@@ -21,12 +21,12 @@ import pathlib
 
 import aiofiles
 import aiofiles.os
-import quart
-import werkzeug.wrappers.response as response
-
 import asfquart.auth as auth
 import asfquart.base as base
 import asfquart.session as session
+import quart
+import werkzeug.wrappers.response as response
+
 import atr.db as db
 import atr.routes as routes
 import atr.util as util
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index 44356ef..e6549a0 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -29,16 +29,16 @@ import shutil
 import tempfile
 from collections.abc import AsyncGenerator, Sequence
 
+import asfquart as asfquart
+import asfquart.auth as auth
+import asfquart.base as base
+import asfquart.session as session
 import cryptography.hazmat.primitives.serialization as serialization
 import gnupg
 import quart
 import werkzeug.wrappers.response as response
 import wtforms
 
-import asfquart as asfquart
-import asfquart.auth as auth
-import asfquart.base as base
-import asfquart.session as session
 import atr.db as db
 import atr.db.models as models
 import atr.routes as routes
diff --git a/atr/routes/package.py b/atr/routes/package.py
index 4f65146..7721622 100644
--- a/atr/routes/package.py
+++ b/atr/routes/package.py
@@ -28,13 +28,13 @@ from collections.abc import Sequence
 
 import aiofiles
 import aiofiles.os
+import asfquart.auth as auth
+import asfquart.base as base
+import asfquart.session as session
 import quart
 import werkzeug.datastructures as datastructures
 import werkzeug.wrappers.response as response
 
-import asfquart.auth as auth
-import asfquart.base as base
-import asfquart.session as session
 import atr.db as db
 import atr.db.models as models
 import atr.routes as routes
diff --git a/atr/routes/projects.py b/atr/routes/projects.py
index be98a24..2600aa0 100644
--- a/atr/routes/projects.py
+++ b/atr/routes/projects.py
@@ -19,12 +19,12 @@
 
 import http.client
 
+import asfquart.base as base
+import asfquart.session as session
 import quart
 import werkzeug.wrappers.response as response
 import wtforms
 
-import asfquart.base as base
-import asfquart.session as session
 import atr.db as db
 import atr.db.models as models
 import atr.routes as routes
diff --git a/atr/routes/release.py b/atr/routes/release.py
index bb0f9fa..d9e5565 100644
--- a/atr/routes/release.py
+++ b/atr/routes/release.py
@@ -21,13 +21,13 @@ import logging
 import logging.handlers
 import pathlib
 
-import quart
-import werkzeug.wrappers.response as response
-
 import asfquart
 import asfquart.auth as auth
 import asfquart.base as base
 import asfquart.session as session
+import quart
+import werkzeug.wrappers.response as response
+
 import atr.db as db
 import atr.db.models as models
 import atr.db.service as service
diff --git a/atr/routes/vote_policy.py b/atr/routes/vote_policy.py
index f447235..8d30d5c 100644
--- a/atr/routes/vote_policy.py
+++ b/atr/routes/vote_policy.py
@@ -17,12 +17,12 @@
 
 """vote_policy.py"""
 
+import asfquart.base as base
+import asfquart.session as session
 import quart
 import werkzeug.wrappers.response as response
 import wtforms
 
-import asfquart.base as base
-import asfquart.session as session
 import atr.db as db
 import atr.routes as routes
 import atr.util as util
diff --git a/atr/server.py b/atr/server.py
index fd7a711..0355bc1 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -22,15 +22,15 @@ import os
 from collections.abc import Iterable
 from typing import Any
 
+import asfquart
+import asfquart.base as base
+import asfquart.generics
+import asfquart.session
 import blockbuster
 import quart
 import quart_schema
 import werkzeug.routing as routing
 
-import asfquart
-import asfquart.base as base
-import asfquart.generics
-import asfquart.session
 import atr.blueprints as blueprints
 import atr.config as config
 import atr.db as db
diff --git a/poetry.lock b/poetry.lock
index 2e3395a..f3ed529 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -216,25 +216,29 @@ trio = ["trio (>=0.26.1)"]
 
 [[package]]
 name = "asfpy"
-version = "0.52"
+version = "0.55"
 description = "ASF Common Python Methods"
 optional = false
-python-versions = "*"
+python-versions = "<4,>=3.9.2"
 groups = ["main"]
 files = [
-    {file = "asfpy-0.52-py3-none-any.whl", hash = 
"sha256:9173e957a11447ddd66ae5d928c1aad03c3cb3e67074bb68b7d637333f39f274"},
-    {file = "asfpy-0.52.tar.gz", hash = 
"sha256:b7c7320f9626935d666800a55fc4af110950907b1ef7521fda667639000079de"},
+    {file = "asfpy-0.55-py3-none-any.whl", hash = 
"sha256:93839b861bc4a7c390d7c8baa0ab8d943462121ecc1151d5c781b40e86226627"},
+    {file = "asfpy-0.55.tar.gz", hash = 
"sha256:a9734ca8827076ca4d8a55a44476a1c54a31ffcefd6f3b3dd01694a0d2d77672"},
 ]
 
 [package.dependencies]
-aiohttp = "*"
-asyncinotify = "*"
-ezt = "*"
-requests = "*"
+aiohttp = ">=3.10.5,<4.0.0"
+cffi = ">=1.17.1,<2.0.0"
+cryptography = ">=44.0.2,<45.0.0"
+easydict = ">=1.13,<2.0"
+ezt = ">=1.1,<2.0"
+pyyaml = ">=6.0.2,<7.0.0"
+requests = ">=2.32.3,<3.0.0"
+watchfiles = ">=1.0.0,<2.0.0"
 
 [package.extras]
-aioldap = ["bonsai"]
-ldap = ["python-ldap"]
+aioldap = ["bonsai (>=1.5.3,<2.0.0)"]
+ldap = ["python-ldap (>=3.4.4,<4.0.0)"]
 
 [[package]]
 name = "asfquart"
@@ -244,32 +248,25 @@ optional = false
 python-versions = ">=3.10,<4"
 groups = ["main"]
 files = []
-develop = true
+develop = false
 
 [package.dependencies]
 aiohttp = "^3.9.2"
-asfpy = "~0.52"
+asfpy = "~0.55"
 easydict = "~1.13"
 ezt = "~1.1"
 PyYAML = "^6.0.1"
 quart = "^0.20.0"
-watchfiles = "~0.24.0"
+watchfiles = "~1.0.0"
 
-[package.source]
-type = "directory"
-url = "asfquart"
+[package.extras]
+aioldap = ["bonsai"]
 
-[[package]]
-name = "asyncinotify"
-version = "4.2.0"
-description = "A simple optionally-async python inotify library, focused on 
simplicity of use and operation, and leveraging modern Python features"
-optional = false
-python-versions = "<4,>=3.6"
-groups = ["main"]
-files = [
-    {file = "asyncinotify-4.2.0-py3-none-any.whl", hash = 
"sha256:23cbcb0704cc65a2009d5ddc5a70dc5be6560708d8a684bba82e03e384c6295f"},
-    {file = "asyncinotify-4.2.0.tar.gz", hash = 
"sha256:dac1d75e16a4919c6eab84a90ff51218db622c5524a84a5c501a0b62ea7ec7ea"},
-]
+[package.source]
+type = "git"
+url = "https://github.com/apache/infrastructure-asfquart.git";
+reference = "bump-asfpy"
+resolved_reference = "a4abe40f22135bfed3b945bf96c2c2a6ba9a7295"
 
 [[package]]
 name = "asyncssh"
@@ -362,7 +359,6 @@ description = "Foreign Function Interface for Python 
calling C code."
 optional = false
 python-versions = ">=3.8"
 groups = ["main"]
-markers = "platform_python_implementation != \"PyPy\""
 files = [
     {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = 
"sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
     {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = 
"sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
@@ -1771,7 +1767,6 @@ description = "C parser in Python"
 optional = false
 python-versions = ">=3.8"
 groups = ["main"]
-markers = "platform_python_implementation != \"PyPy\""
 files = [
     {file = "pycparser-2.22-py3-none-any.whl", hash = 
"sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
     {file = "pycparser-2.22.tar.gz", hash = 
"sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
@@ -2530,95 +2525,83 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", 
"coverage-enable-subprocess
 
 [[package]]
 name = "watchfiles"
-version = "0.24.0"
+version = "1.0.4"
 description = "Simple, modern and high performance file watching and code 
reload in python."
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 groups = ["main"]
 files = [
-    {file = "watchfiles-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = 
"sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0"},
-    {file = "watchfiles-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = 
"sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c"},
-    {file = 
"watchfiles-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
 hash = 
"sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361"},
-    {file = 
"watchfiles-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", 
hash = 
"sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3"},
-    {file = 
"watchfiles-0.24.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", 
hash = 
"sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571"},
-    {file = 
"watchfiles-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",
 hash = 
"sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd"},
-    {file = 
"watchfiles-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", 
hash = 
"sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a"},
-    {file = 
"watchfiles-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", 
hash = 
"sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e"},
-    {file = "watchfiles-0.24.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = 
"sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c"},
-    {file = "watchfiles-0.24.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = 
"sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188"},
-    {file = "watchfiles-0.24.0-cp310-none-win32.whl", hash = 
"sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735"},
-    {file = "watchfiles-0.24.0-cp310-none-win_amd64.whl", hash = 
"sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04"},
-    {file = "watchfiles-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = 
"sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428"},
-    {file = "watchfiles-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = 
"sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c"},
-    {file = 
"watchfiles-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
 hash = 
"sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43"},
-    {file = 
"watchfiles-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", 
hash = 
"sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327"},
-    {file = 
"watchfiles-0.24.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", 
hash = 
"sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5"},
-    {file = 
"watchfiles-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",
 hash = 
"sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61"},
-    {file = 
"watchfiles-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", 
hash = 
"sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15"},
-    {file = 
"watchfiles-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", 
hash = 
"sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823"},
-    {file = "watchfiles-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = 
"sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab"},
-    {file = "watchfiles-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = 
"sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec"},
-    {file = "watchfiles-0.24.0-cp311-none-win32.whl", hash = 
"sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d"},
-    {file = "watchfiles-0.24.0-cp311-none-win_amd64.whl", hash = 
"sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c"},
-    {file = "watchfiles-0.24.0-cp311-none-win_arm64.whl", hash = 
"sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633"},
-    {file = "watchfiles-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = 
"sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a"},
-    {file = "watchfiles-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = 
"sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370"},
-    {file = 
"watchfiles-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
 hash = 
"sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6"},
-    {file = 
"watchfiles-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", 
hash = 
"sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b"},
-    {file = 
"watchfiles-0.24.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", 
hash = 
"sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e"},
-    {file = 
"watchfiles-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",
 hash = 
"sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea"},
-    {file = 
"watchfiles-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", 
hash = 
"sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f"},
-    {file = 
"watchfiles-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", 
hash = 
"sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234"},
-    {file = "watchfiles-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = 
"sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef"},
-    {file = "watchfiles-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = 
"sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968"},
-    {file = "watchfiles-0.24.0-cp312-none-win32.whl", hash = 
"sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444"},
-    {file = "watchfiles-0.24.0-cp312-none-win_amd64.whl", hash = 
"sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896"},
-    {file = "watchfiles-0.24.0-cp312-none-win_arm64.whl", hash = 
"sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418"},
-    {file = "watchfiles-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = 
"sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48"},
-    {file = "watchfiles-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = 
"sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90"},
-    {file = 
"watchfiles-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
 hash = 
"sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94"},
-    {file = 
"watchfiles-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", 
hash = 
"sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e"},
-    {file = 
"watchfiles-0.24.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", 
hash = 
"sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827"},
-    {file = 
"watchfiles-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",
 hash = 
"sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df"},
-    {file = 
"watchfiles-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", 
hash = 
"sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab"},
-    {file = 
"watchfiles-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", 
hash = 
"sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f"},
-    {file = "watchfiles-0.24.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = 
"sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b"},
-    {file = "watchfiles-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = 
"sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18"},
-    {file = "watchfiles-0.24.0-cp313-none-win32.whl", hash = 
"sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07"},
-    {file = "watchfiles-0.24.0-cp313-none-win_amd64.whl", hash = 
"sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366"},
-    {file = "watchfiles-0.24.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = 
"sha256:ee82c98bed9d97cd2f53bdb035e619309a098ea53ce525833e26b93f673bc318"},
-    {file = "watchfiles-0.24.0-cp38-cp38-macosx_11_0_arm64.whl", hash = 
"sha256:fd92bbaa2ecdb7864b7600dcdb6f2f1db6e0346ed425fbd01085be04c63f0b05"},
-    {file = 
"watchfiles-0.24.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", 
hash = 
"sha256:f83df90191d67af5a831da3a33dd7628b02a95450e168785586ed51e6d28943c"},
-    {file = 
"watchfiles-0.24.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", 
hash = 
"sha256:fca9433a45f18b7c779d2bae7beeec4f740d28b788b117a48368d95a3233ed83"},
-    {file = 
"watchfiles-0.24.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash 
= "sha256:b995bfa6bf01a9e09b884077a6d37070464b529d8682d7691c2d3b540d357a0c"},
-    {file = 
"watchfiles-0.24.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", 
hash = 
"sha256:ed9aba6e01ff6f2e8285e5aa4154e2970068fe0fc0998c4380d0e6278222269b"},
-    {file = 
"watchfiles-0.24.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", 
hash = 
"sha256:e5171ef898299c657685306d8e1478a45e9303ddcd8ac5fed5bd52ad4ae0b69b"},
-    {file = 
"watchfiles-0.24.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", 
hash = 
"sha256:4933a508d2f78099162da473841c652ad0de892719043d3f07cc83b33dfd9d91"},
-    {file = "watchfiles-0.24.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = 
"sha256:95cf3b95ea665ab03f5a54765fa41abf0529dbaf372c3b83d91ad2cfa695779b"},
-    {file = "watchfiles-0.24.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = 
"sha256:01def80eb62bd5db99a798d5e1f5f940ca0a05986dcfae21d833af7a46f7ee22"},
-    {file = "watchfiles-0.24.0-cp38-none-win32.whl", hash = 
"sha256:4d28cea3c976499475f5b7a2fec6b3a36208656963c1a856d328aeae056fc5c1"},
-    {file = "watchfiles-0.24.0-cp38-none-win_amd64.whl", hash = 
"sha256:21ab23fdc1208086d99ad3f69c231ba265628014d4aed31d4e8746bd59e88cd1"},
-    {file = "watchfiles-0.24.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = 
"sha256:b665caeeda58625c3946ad7308fbd88a086ee51ccb706307e5b1fa91556ac886"},
-    {file = "watchfiles-0.24.0-cp39-cp39-macosx_11_0_arm64.whl", hash = 
"sha256:5c51749f3e4e269231510da426ce4a44beb98db2dce9097225c338f815b05d4f"},
-    {file = 
"watchfiles-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", 
hash = 
"sha256:82b2509f08761f29a0fdad35f7e1638b8ab1adfa2666d41b794090361fb8b855"},
-    {file = 
"watchfiles-0.24.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", 
hash = 
"sha256:9a60e2bf9dc6afe7f743e7c9b149d1fdd6dbf35153c78fe3a14ae1a9aee3d98b"},
-    {file = 
"watchfiles-0.24.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash 
= "sha256:f7d9b87c4c55e3ea8881dfcbf6d61ea6775fffed1fedffaa60bd047d3c08c430"},
-    {file = 
"watchfiles-0.24.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", 
hash = 
"sha256:78470906a6be5199524641f538bd2c56bb809cd4bf29a566a75051610bc982c3"},
-    {file = 
"watchfiles-0.24.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", 
hash = 
"sha256:07cdef0c84c03375f4e24642ef8d8178e533596b229d32d2bbd69e5128ede02a"},
-    {file = 
"watchfiles-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", 
hash = 
"sha256:d337193bbf3e45171c8025e291530fb7548a93c45253897cd764a6a71c937ed9"},
-    {file = "watchfiles-0.24.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = 
"sha256:ec39698c45b11d9694a1b635a70946a5bad066b593af863460a8e600f0dff1ca"},
-    {file = "watchfiles-0.24.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = 
"sha256:2e28d91ef48eab0afb939fa446d8ebe77e2f7593f5f463fd2bb2b14132f95b6e"},
-    {file = "watchfiles-0.24.0-cp39-none-win32.whl", hash = 
"sha256:7138eff8baa883aeaa074359daabb8b6c1e73ffe69d5accdc907d62e50b1c0da"},
-    {file = "watchfiles-0.24.0-cp39-none-win_amd64.whl", hash = 
"sha256:b3ef2c69c655db63deb96b3c3e587084612f9b1fa983df5e0c3379d41307467f"},
-    {file = "watchfiles-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", 
hash = 
"sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f"},
-    {file = "watchfiles-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash 
= "sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b"},
-    {file = 
"watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
 hash = 
"sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4"},
-    {file = 
"watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
 hash = 
"sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a"},
-    {file = "watchfiles-0.24.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash 
= "sha256:96619302d4374de5e2345b2b622dc481257a99431277662c30f606f3e22f42be"},
-    {file = "watchfiles-0.24.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = 
"sha256:85d5f0c7771dcc7a26c7a27145059b6bb0ce06e4e751ed76cdf123d7039b60b5"},
-    {file = 
"watchfiles-0.24.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
 hash = 
"sha256:951088d12d339690a92cef2ec5d3cfd957692834c72ffd570ea76a6790222777"},
-    {file = 
"watchfiles-0.24.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
 hash = 
"sha256:49fb58bcaa343fedc6a9e91f90195b20ccb3135447dc9e4e2570c3a39565853e"},
-    {file = "watchfiles-0.24.0.tar.gz", hash = 
"sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1"},
+    {file = "watchfiles-1.0.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = 
"sha256:ba5bb3073d9db37c64520681dd2650f8bd40902d991e7b4cfaeece3e32561d08"},
+    {file = "watchfiles-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = 
"sha256:9f25d0ba0fe2b6d2c921cf587b2bf4c451860086534f40c384329fb96e2044d1"},
+    {file = 
"watchfiles-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
 hash = 
"sha256:47eb32ef8c729dbc4f4273baece89398a4d4b5d21a1493efea77a17059f4df8a"},
+    {file = 
"watchfiles-1.0.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", 
hash = 
"sha256:076f293100db3b0b634514aa0d294b941daa85fc777f9c698adb1009e5aca0b1"},
+    {file = 
"watchfiles-1.0.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash 
= "sha256:1eacd91daeb5158c598fe22d7ce66d60878b6294a86477a4715154990394c9b3"},
+    {file = 
"watchfiles-1.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",
 hash = 
"sha256:13c2ce7b72026cfbca120d652f02c7750f33b4c9395d79c9790b27f014c8a5a2"},
+    {file = 
"watchfiles-1.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", 
hash = 
"sha256:90192cdc15ab7254caa7765a98132a5a41471cf739513cc9bcf7d2ffcc0ec7b2"},
+    {file = 
"watchfiles-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", 
hash = 
"sha256:278aaa395f405972e9f523bd786ed59dfb61e4b827856be46a42130605fd0899"},
+    {file = "watchfiles-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = 
"sha256:a462490e75e466edbb9fc4cd679b62187153b3ba804868452ef0577ec958f5ff"},
+    {file = "watchfiles-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = 
"sha256:8d0d0630930f5cd5af929040e0778cf676a46775753e442a3f60511f2409f48f"},
+    {file = "watchfiles-1.0.4-cp310-cp310-win32.whl", hash = 
"sha256:cc27a65069bcabac4552f34fd2dce923ce3fcde0721a16e4fb1b466d63ec831f"},
+    {file = "watchfiles-1.0.4-cp310-cp310-win_amd64.whl", hash = 
"sha256:8b1f135238e75d075359cf506b27bf3f4ca12029c47d3e769d8593a2024ce161"},
+    {file = "watchfiles-1.0.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = 
"sha256:2a9f93f8439639dc244c4d2902abe35b0279102bca7bbcf119af964f51d53c19"},
+    {file = "watchfiles-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = 
"sha256:9eea33ad8c418847dd296e61eb683cae1c63329b6d854aefcd412e12d94ee235"},
+    {file = 
"watchfiles-1.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
 hash = 
"sha256:31f1a379c9dcbb3f09cf6be1b7e83b67c0e9faabed0471556d9438a4a4e14202"},
+    {file = 
"watchfiles-1.0.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", 
hash = 
"sha256:ab594e75644421ae0a2484554832ca5895f8cab5ab62de30a1a57db460ce06c6"},
+    {file = 
"watchfiles-1.0.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash 
= "sha256:fc2eb5d14a8e0d5df7b36288979176fbb39672d45184fc4b1c004d7c3ce29317"},
+    {file = 
"watchfiles-1.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",
 hash = 
"sha256:3f68d8e9d5a321163ddacebe97091000955a1b74cd43724e346056030b0bacee"},
+    {file = 
"watchfiles-1.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", 
hash = 
"sha256:f9ce064e81fe79faa925ff03b9f4c1a98b0bbb4a1b8c1b015afa93030cb21a49"},
+    {file = 
"watchfiles-1.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", 
hash = 
"sha256:b77d5622ac5cc91d21ae9c2b284b5d5c51085a0bdb7b518dba263d0af006132c"},
+    {file = "watchfiles-1.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = 
"sha256:1941b4e39de9b38b868a69b911df5e89dc43767feeda667b40ae032522b9b5f1"},
+    {file = "watchfiles-1.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = 
"sha256:4f8c4998506241dedf59613082d1c18b836e26ef2a4caecad0ec41e2a15e4226"},
+    {file = "watchfiles-1.0.4-cp311-cp311-win32.whl", hash = 
"sha256:4ebbeca9360c830766b9f0df3640b791be569d988f4be6c06d6fae41f187f105"},
+    {file = "watchfiles-1.0.4-cp311-cp311-win_amd64.whl", hash = 
"sha256:05d341c71f3d7098920f8551d4df47f7b57ac5b8dad56558064c3431bdfc0b74"},
+    {file = "watchfiles-1.0.4-cp311-cp311-win_arm64.whl", hash = 
"sha256:32b026a6ab64245b584acf4931fe21842374da82372d5c039cba6bf99ef722f3"},
+    {file = "watchfiles-1.0.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = 
"sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2"},
+    {file = "watchfiles-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = 
"sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9"},
+    {file = 
"watchfiles-1.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
 hash = 
"sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712"},
+    {file = 
"watchfiles-1.0.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", 
hash = 
"sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12"},
+    {file = 
"watchfiles-1.0.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash 
= "sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844"},
+    {file = 
"watchfiles-1.0.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",
 hash = 
"sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733"},
+    {file = 
"watchfiles-1.0.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", 
hash = 
"sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af"},
+    {file = 
"watchfiles-1.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", 
hash = 
"sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a"},
+    {file = "watchfiles-1.0.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = 
"sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff"},
+    {file = "watchfiles-1.0.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = 
"sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e"},
+    {file = "watchfiles-1.0.4-cp312-cp312-win32.whl", hash = 
"sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94"},
+    {file = "watchfiles-1.0.4-cp312-cp312-win_amd64.whl", hash = 
"sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c"},
+    {file = "watchfiles-1.0.4-cp312-cp312-win_arm64.whl", hash = 
"sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90"},
+    {file = "watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = 
"sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9"},
+    {file = "watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = 
"sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60"},
+    {file = 
"watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
 hash = 
"sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407"},
+    {file = 
"watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", 
hash = 
"sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d"},
+    {file = 
"watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash 
= "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d"},
+    {file = 
"watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",
 hash = 
"sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b"},
+    {file = 
"watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", 
hash = 
"sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590"},
+    {file = 
"watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", 
hash = 
"sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902"},
+    {file = "watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = 
"sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1"},
+    {file = "watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = 
"sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303"},
+    {file = "watchfiles-1.0.4-cp313-cp313-win32.whl", hash = 
"sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80"},
+    {file = "watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = 
"sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc"},
+    {file = "watchfiles-1.0.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = 
"sha256:d3452c1ec703aa1c61e15dfe9d482543e4145e7c45a6b8566978fbb044265a21"},
+    {file = "watchfiles-1.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = 
"sha256:7b75fee5a16826cf5c46fe1c63116e4a156924d668c38b013e6276f2582230f0"},
+    {file = 
"watchfiles-1.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", 
hash = 
"sha256:4e997802d78cdb02623b5941830ab06f8860038faf344f0d288d325cc9c5d2ff"},
+    {file = 
"watchfiles-1.0.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", 
hash = 
"sha256:e0611d244ce94d83f5b9aff441ad196c6e21b55f77f3c47608dcf651efe54c4a"},
+    {file = 
"watchfiles-1.0.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = 
"sha256:9745a4210b59e218ce64c91deb599ae8775c8a9da4e95fb2ee6fe745fc87d01a"},
+    {file = 
"watchfiles-1.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", 
hash = 
"sha256:4810ea2ae622add560f4aa50c92fef975e475f7ac4900ce5ff5547b2434642d8"},
+    {file = 
"watchfiles-1.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash 
= "sha256:740d103cd01458f22462dedeb5a3382b7f2c57d07ff033fbc9465919e5e1d0f3"},
+    {file = 
"watchfiles-1.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", 
hash = 
"sha256:cdbd912a61543a36aef85e34f212e5d2486e7c53ebfdb70d1e0b060cc50dd0bf"},
+    {file = "watchfiles-1.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = 
"sha256:0bc80d91ddaf95f70258cf78c471246846c1986bcc5fd33ccc4a1a67fcb40f9a"},
+    {file = "watchfiles-1.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = 
"sha256:ab0311bb2ffcd9f74b6c9de2dda1612c13c84b996d032cd74799adb656af4e8b"},
+    {file = "watchfiles-1.0.4-cp39-cp39-win32.whl", hash = 
"sha256:02a526ee5b5a09e8168314c905fc545c9bc46509896ed282aeb5a8ba9bd6ca27"},
+    {file = "watchfiles-1.0.4-cp39-cp39-win_amd64.whl", hash = 
"sha256:a5ae5706058b27c74bac987d615105da17724172d5aaacc6c362a40599b6de43"},
+    {file = "watchfiles-1.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", 
hash = 
"sha256:cdcc92daeae268de1acf5b7befcd6cfffd9a047098199056c72e4623f531de18"},
+    {file = "watchfiles-1.0.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash 
= "sha256:d8d3d9203705b5797f0af7e7e5baa17c8588030aaadb7f6a86107b7247303817"},
+    {file = 
"watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
 hash = 
"sha256:bdef5a1be32d0b07dcea3318a0be95d42c98ece24177820226b56276e06b63b0"},
+    {file = 
"watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
 hash = 
"sha256:342622287b5604ddf0ed2d085f3a589099c9ae8b7331df3ae9845571586c4f3d"},
+    {file = "watchfiles-1.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash 
= "sha256:9fe37a2de80aa785d340f2980276b17ef697ab8db6019b07ee4fd28a8359d2f3"},
+    {file = "watchfiles-1.0.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = 
"sha256:9d1ef56b56ed7e8f312c934436dea93bfa3e7368adfcf3df4c0da6d4de959a1e"},
+    {file = 
"watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
 hash = 
"sha256:95b42cac65beae3a362629950c444077d1b44f1790ea2772beaea95451c086bb"},
+    {file = 
"watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
 hash = 
"sha256:5e0227b8ed9074c6172cf55d85b5670199c99ab11fd27d2c473aa30aec67ee42"},
+    {file = "watchfiles-1.0.4.tar.gz", hash = 
"sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205"},
 ]
 
 [package.dependencies]
@@ -2775,4 +2758,4 @@ propcache = ">=0.2.0"
 [metadata]
 lock-version = "2.1"
 python-versions = "~=3.13"
-content-hash = 
"d36d2d586ed32d0c1a2e1a13639c4e089fb5cead77d7c6a4cb41448e0435993b"
+content-hash = 
"cf228cf90230100969f7433a105d889cdb96428f3e0ac5954997b373f9818324"
diff --git a/pyproject.toml b/pyproject.toml
index ca597b1..9b43a2c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,7 +13,7 @@ dependencies = [
   "aiofiles>=24.1.0,<25.0.0",
   "aiosqlite>=0.21.0,<0.22.0",
   "alembic~=1.14",
-  "asfquart", # TODO: convert asfquart from a source dependency to pypi or git 
dependency
+  "asfquart @ 
git+https://github.com/apache/infrastructure-asfquart.git@bump-asfpy";, # TODO: 
change to main once the associated PR is merged
   "asyncssh>=2.20.0,<3.0.0",
   "blockbuster>=1.5.23,<2.0.0",
   "cryptography~=44.0",
@@ -28,7 +28,7 @@ dependencies = [
   "quart-schema[pydantic]~=0.21",
   "quart-wtforms~=1.0.3",
   "email-validator~=2.2.0",
-  "sqlmodel~=0.0",
+  "sqlmodel~=0.0.24",
 ]
 
 [dependency-groups]
@@ -54,13 +54,6 @@ test = [
 [tool.poetry]
 package-mode = false
 
-# When both project.dependencies and tool.poetry.dependencies are specified,
-# project.dependencies are used for metadata when building the project,
-# tool.poetry.dependencies is only used to enrich project.dependencies for 
locking.
-# so we only need to enrich the dependencies with the source location of 
asfquart
-[tool.poetry.dependencies]
-asfquart = { path = "./asfquart", develop = true }
-
 [tool.poetry.group.test.dependencies]
 pytest = ">=8.0"
 pytest-asyncio = ">=0.24"
@@ -83,7 +76,6 @@ exclude = [
   "**/node_modules",
   "**/__pycache__",
   ".venv*",
-  "asfquart",
   "tests",
   "atr/util.py"
 ]
@@ -98,7 +90,6 @@ executionEnvironments = [
 ]
 
 [tool.ruff]
-exclude = ["asfquart"]
 line-length = 120
 
 [tool.ruff.lint]
@@ -124,7 +115,7 @@ select = [
 
 [tool.mypy]
 python_version = "3.13"
-exclude = ["asfquart", "tests"]
+exclude = ["tests"]
 mypy_path = "typestubs"
 check_untyped_defs = false
 disallow_incomplete_defs = true


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to