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]