Commit graph

46 commits

Author SHA1 Message Date
Zanie Blue
af9c2e60aa
Ignore py not found errors during interpreter discovery (#4620)
Closes https://github.com/astral-sh/uv/issues/4522
2024-06-28 23:39:26 -05:00
Zanie Blue
3a62ba3809
Improve toolchain and environment missing error messages (#4596)
The journey here can be seen in:

- #4587 
- #4589 
- #4594 

I collapsed all the commits here because only the last one in the stack
got us to a "correct" error message.

There are a few architectural changes:

- We have a dedicated `MissingEnvironment` and `EnvironmentNotFound`
type for `PythonEnvironment::find` allowing different error messages
when searching for environments
- `ToolchainNotFound` becomes a struct with the `ToolchainRequest` which
greatly simplifies missing toolchain error formatting
- `ToolchainNotFound` tracks the `EnvironmentPreference` so it can
accurately report the locations checked

The messages look like this now, instead of the bland (and often
incorrect): "No Python interpreter found in system toolchains".

```
❯ cargo run -q -- pip sync requirements.txt
error: No virtual environment found
❯ UV_TEST_PYTHON_PATH="" cargo run -q -- pip sync requirements.txt --system
error: No system environment found
❯ UV_TEST_PYTHON_PATH="" cargo run -q -- pip sync requirements.txt --python 3.12
error: No virtual environment found for Python 3.12
❯ UV_TEST_PYTHON_PATH="" cargo run -q -- pip sync requirements.txt --python 3.12 --system
error: No system environment found for Python 3.12
❯ UV_TEST_PYTHON_PATH="" cargo run -q -- toolchain find 3.12 --preview
error: No toolchain found for Python 3.12 in system path
❯ UV_TEST_PYTHON_PATH="" cargo run -q -- pip compile requirements.in
error: No toolchain found in virtual environments or system path
```

I'd like to follow this with hints, suggesting creating an environment
or using system in some cases.
2024-06-28 15:16:59 +00:00
Zanie Blue
363f3f7862
Replace all uses of fs_err::tokio::rename with uv_fs::rename_with_retry (#4606)
Same as #4605 but includes persistence of extracted archives and
toolchain downloads.
2024-06-28 14:33:23 +00:00
Zanie Blue
c9657b0015
Add uv tool install (#4492)
This is the minimal "working" implementation. In summary, we:

- Resolve the requested requirements
- Create an environment at `$UV_STATE_DIR/tools/$name`
- Inspect the `dist-info` for the main requirement to determine its
entry points scripts
- Link the entry points from a user-executable directory
(`$XDG_BIN_HOME`) to the environment bin
- Create an entry at `$UV_STATE_DIR/tools/tools.toml` tracking the
user's request

The idea with `tools.toml` is that it allows us to perform upgrades and
syncs, retaining the original user request (similar to declarations in a
`pyproject.toml`). I imagine using a similar schema in the
`pyproject.toml` in the future if/when we add project-levle tools. I'm
also considering exposing `tools.toml` in the standard uv configuration
directory instead of the state directory, but it seems nice to tuck it
away for now while we iterate on it. Installing a tool won't perform a
sync of other tool environments, we'll probably have an explicit `uv
tool sync` command for that?

I've split out todos into follow-up pull requests:

- #4509 (failing on Windows)
- #4501 
- #4504 

Closes #4485
2024-06-26 10:24:29 -05:00
Eric Mark Martin
967f136564
More precise locking with --prefix option (#4506)
## Summary

In #4085, support was implemented for the `--prefix` option. When using
this option, however, a lock is either acquired on the virtualenv or
globally, preventing multiple installs to different `--prefix`s from the
same interpreter.

In this change, acquire the lock on just the prefix in question.

## Test Plan

Ran a `uv pip install` with `--prefix` and `RUST_LOG=trace` and observed
that the lock was acquired in the prefix.
2024-06-25 06:47:52 -04:00
Zanie Blue
8f2d8b3f4f
Fix ordering of prefer-system toolchain preference (#4441)
Whoopsies!
2024-06-22 09:42:51 -05:00
Zanie Blue
93c6e0df56
Expose toolchain-preference as a CLI and configuration file option (#4424)
Exposes the option added in #4416. Adds `--toolchain-preference` and
`tool.uv.toolchain-preference` to configure if system or managed
toolchains are preferred. Users can opt-out of managed toolchains or
system toolchains entirely as well.
2024-06-20 13:42:09 -05:00
Zanie Blue
e783a79955
Add PythonEnvironment::find API (#4423)
Restores the `PythonEnvironment::find` API which was removed a while
back in favor of `Toolchain::find`. As mentioned in #4416, I'm
attempting to separate the case where you want an active environment
from the case where you want an installed toolchain in order to create
environments.

I wanted to drop `EnvironmentPreference` from `Toolchain::find` and just
have us consistently consider (or not consider) virtual environments
when discovering toolchains for creating environments. Unfortunately
this caused a few things to break so I reverted that change and will
explore it separately. Because I was exploring that change, there are
some minor changes to the `Toolchain` API here.
2024-06-20 17:54:17 +00:00
Zanie Blue
13e532ccda
Add internal options for managing toolchain discovery preferences (#4416)
Adds support for the toolchain discovery preferences outlined in
https://github.com/astral-sh/uv/issues/4198 but we don't expose this to
users yet, I'll do that next to make it easier to review.

I've made some refactors in the toolchain discovery implementation to
enable this behavior and move us towards clearer abstractions. There's
still remaining work here, but I'd prefer tackle things in follow-ups
instead of expanding this pull request. I plan on opening a couple
before merging this.

I'd like to shift the public toolchain API to focus on discovering
either an **environment** or a **toolchain**. The first would be used by
commands that operate on an environment, while the latter would be used
by commands that just need an interpreter to create environments. I
haven't changed this here, but some of the refactors are in preparation
for supporting this idea.

In brief:

- We now allow different ordering of installed toolchain discovery based
on a `ToolchainPreference` type. This is the type we will expose to
users.
- `SystemPython` was changed into an `EnvironmentPreference` which is
used to determine if we should prefer virtual or system Python
environments.
- We drop the whole `ToolchainSources` selection concept, it was
confusing and the error messages from it were awkward. Most of the
functionality is now captured by the preference enums, but you can't do
things like "only find a toolchain from the parent interpreter" as
easily anymore.
2024-06-20 08:57:05 -05:00
Zanie Blue
34c7bc5cc8
Refactor Python executable chain into lazy variables (#4318)
In preparation for changing the order dynamically in #4416
2024-06-19 15:27:02 -05:00
Zanie Blue
a68146d978
Support toolchain requests with platform-tag style Python implementations and version (#4407)
Closes https://github.com/astral-sh/uv/issues/4399

---------

Co-authored-by: Andrew Gallant <andrew@astral.sh>
2024-06-19 17:04:24 +00:00
Zanie Blue
1f2dd5b942
Remove stale comment (#4413) 2024-06-19 15:01:33 +00:00
Zanie Blue
b3e3d899e5
Use &impl AsRef<Path> instead of type parameter (#4383) 2024-06-18 15:54:07 +00:00
Zanie Blue
58f53f01bb
Ignore query errors during uv toolchain list (#4382)
Closes #4380 

This is the same logic as `should_stop_discovery` but I changed the log
level and duplicated it because I don't really want that method to be
public. Maybe it should be though?
2024-06-18 14:52:59 +00:00
Zanie Blue
1ce21475a5
Respect .python-version files and fetch manged toolchains in uv project commands (#4361)
As in #4360, updates the uv project CLI to respect `.python-version`
files as default Python version requests. Additionally, updates project
interpreter discovery to fetch managed toolchains as in `uv venv
--preview`.
2024-06-18 09:43:52 -05:00
Zanie Blue
903dfc2f1f
Respect .python-version in uv venv --preview (#4360)
Adds support for reading Python version files (introduced in #4335) to
`uv venv`. If present, we'll use the file version as the default.
2024-06-18 14:21:35 +00:00
Zanie Blue
56f0a117ca
Move version file reading to uv-toolchain (#4340)
In preparation for using this in other commands.
2024-06-17 18:54:22 +00:00
Zanie Blue
fdcdc2cbe6
Allow multiple toolchains to be requested in uv toolchain install (#4334)
Allows installation of multiple toolchains in a single invocation
because I don't want to be limited to one! Most of the implementation
for concurrent downloads ported from `cargo dev fetch-python`.
2024-06-17 18:24:11 +00:00
Zanie Blue
5248269d27
Add support for toolchain requests by key (#4332)
Adds support for toolchain keys e.g. `cpython-3.11.2-macos` allowing you
to download toolchains for specific architectures and operating systems
using the format we use to uniquely identify a toolchain.
2024-06-17 13:11:24 -05:00
Zanie Blue
b5d280dc40
Refactor Toolchain API to always take ToolchainRequest instead of str (#4341)
This API was taking an `Option<&str>` for caller convenience in some
places but we ought to just take a `ToolchainRequest` consistently.
2024-06-17 11:16:15 -05:00
Zanie Blue
67f1285ce3
Add EXTERNALLY-MANAGED markers to managed toolchains (#4312)
Closes #4240 

e.g.

```
❯ cargo run -q -- pip install anyio --python "/Users/zb/Library/Application Support/uv/toolchains/cpython-3.12.0-macos-aarch64-none/install/bin/python3"
error: The interpreter at /Users/zb/Library/Application Support/uv/toolchains/cpython-3.12.0-macos-aarch64-none/install is externally managed, and indicates the following:

  This toolchain is managed by uv and should not be modified.

Consider creating a virtual environment with `uv venv`.
```
2024-06-17 15:25:34 +00:00
Zanie Blue
accbb9b695
Add uv toolchain find (#4206)
Adds a command to find a toolchain on the system. Right now, it displays
the path to the first matching toolchain. We'll probably have more rich
output in the future (after implementing `toolchain show`).

The eventual plan (separate from here) is to port all of the toolchain
discovery tests to use this command. I'll add a few tests for this
command here anyway.
2024-06-14 17:03:16 +00:00
Charlie Marsh
e0a389032f
Add persistent configuration for non-pip APIs (#4294)
## Summary

This PR introduces top-level configuration for uv, such that you can do:

```toml
[tool.uv]
index-url = "https://test.pypi.org/simple"
```

And `uv pip compile`, `uv run`, `uv tool run`, etc., will all respect
that configuration.

The settings that were escalated to the top-level remain on
`tool.uv.pip` too, but they're only respected in `uv pip` commands. If
they're specified in both places, then the `pip` settings win out.

While making this change, I also wired up some of the global options,
like `connectivity` and `native_tls`, through to all the relevant
places.

Closes #4250.
2024-06-13 20:56:38 -04:00
Zanie Blue
572551c108
Refactor toolchain discovery to use satisfies_system_python explicitly (#4310)
Splitting out the refactor from
https://github.com/astral-sh/uv/pull/4309
2024-06-13 17:44:53 +00:00
Zanie Blue
b07c132ede
Skip invalid interpreters when searching for requested interpreter executable name (#4308)
Previously, we took the first executable on the `PATH` but if it was not
a usable interpreter we'd fail. Now, we'll continue searching in the
path until we find an interpreter as we do with the standard executable
names.
2024-06-13 12:36:12 -05:00
Zanie Blue
89daa51dbe
Add support for listing system toolchains (#4172)
Includes system interpreters in `uv toolchain list`.

This includes a refactor of `find_toolchain` to support iterating over
all toolchains
that match a request rather than ending earlier.
2024-06-13 10:25:30 -05:00
Zanie Blue
b43de79275
Fix incorrect parsing of requested Python version as empty version specifiers (#4289)
Before 0.2.10 we would parse `--python=python` as an executable name.
After https://github.com/astral-sh/uv/pull/4214, we started treating
this as a Python version range request (with an empty version range).
This is not entirely unreasonable, but it was an unexpected regression
and I don't think `VersionRequest` should support empty ranges in its
`from_str` implementation without more consideration.
2024-06-12 19:48:59 -05:00
Zanie Blue
f7f55ede2f
Refactor uv-toolchain::platform to use target-lexicon (#4236)
Closes https://github.com/astral-sh/uv/issues/3857

Instead of using custom `Arch`, `Os`, and `Libc` types I just use
`target-lexicon`'s which enumerate way more variants and implement
display and parsing. We use a wrapper type to represent a couple special
cases to support the "x86" alias for "i686" and "macos" for "darwin".
Alternatively we could try to use our `platform-tags` types but those
capture more information (like operating system versions) that we don't
have for downloads.

As discussed in https://github.com/astral-sh/uv/pull/4160, this is not
sufficient for proper libc detection but that work is larger and will be
handled separately.
2024-06-12 09:11:56 -05:00
Zanie Blue
5f37395f45
Allow version specifiers to be used in Python version requests (#4214)
In service of https://github.com/astral-sh/uv/issues/4212 but this is
user-facing e.g. Python discovery will support version specifiers
everywhere now.

Closes https://github.com/astral-sh/uv/issues/4212
2024-06-10 18:20:09 -05:00
Zanie Blue
98d1ea6bb0
Improve handling of missing interpreters during discovery (#4218)
Cherry-picked from https://github.com/astral-sh/uv/pull/4214

The first commit gets us some context on an IO error during queries:

Previously:

```
failed to canonicalize path `[VENV]/bin/python3`
    Caused by: No such file or directory (os error 2)
```

Now:

```
Failed to query Python interpreter
    Caused by: failed to canonicalize path `[VENV]/bin/python3`
    Caused by: No such file or directory (os error 2)
```

but really we shouldn't attempt to query a missing interpreter during
discovery anyway, so we improve handling of that too.
2024-06-10 22:26:34 +00:00
Zanie Blue
fd52fe74ce
Update the Toolchain::find_requested API to take a parsed request (#4215)
Pulled out of https://github.com/astral-sh/uv/pull/4206, need this for
#4214
2024-06-10 20:07:45 +00:00
Zanie Blue
14d535f384
Add uv toolchain install (#4164)
Adds a command (following #4163) to download and install specific
toolchains. While we fetch toolchains on demand, this is useful for,
e.g., pre-downloading a toolchain in a Docker image build.

~I kind of think we should call this `install` instead of `fetch`~ I
changed the name from `fetch` to `install`.
2024-06-10 10:56:08 -05:00
konsti
90947a933d
Recreate project environment if --python or requires-python doesn't match (#3945)
Fixes #4131
Fixes #3895
2024-06-10 14:36:14 +00:00
Zanie Blue
c6da4f15b7
Add uv toolchain list (#4163)
Adds the `uv toolchain` namespace and a `list` command to get us
started.

```
❯ cargo run -q -- toolchain list
warning: `uv toolchain list` is experimental and may change without warning.
3.8.12   (cpython-3.8.12-macos-aarch64-none)
3.8.13   (cpython-3.8.13-macos-aarch64-none)
3.8.14   (cpython-3.8.14-macos-aarch64-none)
3.8.15   (cpython-3.8.15-macos-aarch64-none)
3.8.16   (cpython-3.8.16-macos-aarch64-none)
3.8.17   (cpython-3.8.17-macos-aarch64-none)
3.8.18   (cpython-3.8.18-macos-aarch64-none)
3.8.18   (cpython-3.8.18-macos-aarch64-none)
3.8.19   (cpython-3.8.19-macos-aarch64-none)
3.9.2    (cpython-3.9.2-macos-aarch64-none)
3.9.3    (cpython-3.9.3-macos-aarch64-none)
3.9.4    (cpython-3.9.4-macos-aarch64-none)
3.9.5    (cpython-3.9.5-macos-aarch64-none)
3.9.6    (cpython-3.9.6-macos-aarch64-none)
3.9.7    (cpython-3.9.7-macos-aarch64-none)
3.9.10   (cpython-3.9.10-macos-aarch64-none)
3.9.11   (cpython-3.9.11-macos-aarch64-none)
3.9.12   (cpython-3.9.12-macos-aarch64-none)
3.9.13   (cpython-3.9.13-macos-aarch64-none)
3.9.14   (cpython-3.9.14-macos-aarch64-none)
3.9.15   (cpython-3.9.15-macos-aarch64-none)
3.9.16   (cpython-3.9.16-macos-aarch64-none)
3.9.17   (cpython-3.9.17-macos-aarch64-none)
3.9.18   (cpython-3.9.18-macos-aarch64-none)
3.9.19   (cpython-3.9.19-macos-aarch64-none)
3.10.0   (cpython-3.10.0-macos-aarch64-none)
3.10.2   (cpython-3.10.2-macos-aarch64-none)
3.10.3   (cpython-3.10.3-macos-aarch64-none)
3.10.4   (cpython-3.10.4-macos-aarch64-none)
3.10.5   (cpython-3.10.5-macos-aarch64-none)
3.10.6   (cpython-3.10.6-macos-aarch64-none)
3.10.7   (cpython-3.10.7-macos-aarch64-none)
3.10.8   (cpython-3.10.8-macos-aarch64-none)
3.10.9   (cpython-3.10.9-macos-aarch64-none)
3.10.11  (cpython-3.10.11-macos-aarch64-none)
3.10.12  (cpython-3.10.12-macos-aarch64-none)
3.10.13  (cpython-3.10.13-macos-aarch64-none)
3.10.14  (cpython-3.10.14-macos-aarch64-none)
3.11.1   (cpython-3.11.1-macos-aarch64-none)
3.11.3   (cpython-3.11.3-macos-aarch64-none)
3.11.4   (cpython-3.11.4-macos-aarch64-none)
3.11.5   (cpython-3.11.5-macos-aarch64-none)
3.11.6   (cpython-3.11.6-macos-aarch64-none)
3.11.7   (cpython-3.11.7-macos-aarch64-none)
3.11.8   (cpython-3.11.8-macos-aarch64-none)
3.11.9   (cpython-3.11.9-macos-aarch64-none)
3.12.0   (cpython-3.12.0-macos-aarch64-none)
3.12.1   (cpython-3.12.1-macos-aarch64-none)
3.12.2   (cpython-3.12.2-macos-aarch64-none)
3.12.3   (cpython-3.12.3-macos-aarch64-none)
```

Closes https://github.com/astral-sh/uv/issues/4189
2024-06-10 09:22:00 -05:00
Zanie Blue
45df889fe4
Implement Toolchain::find_or_fetch and use in uv venv --preview (#4138)
Extends https://github.com/astral-sh/uv/pull/4121
Part of #2607 

Adds support for managed toolchain fetching to `uv venv`, e.g.

```
❯ cargo run -q -- venv --python 3.9.18 --preview -v
DEBUG Searching for Python 3.9.18 in search path or managed toolchains
DEBUG Searching for managed toolchains at `/Users/zb/Library/Application Support/uv/toolchains`
DEBUG Found CPython 3.12.3 at `/opt/homebrew/bin/python3` (search path)
DEBUG Found CPython 3.9.6 at `/usr/bin/python3` (search path)
DEBUG Found CPython 3.12.3 at `/opt/homebrew/bin/python3` (search path)
DEBUG Requested Python not found, checking for available download...
DEBUG Using registry request timeout of 30s
INFO Fetching requested toolchain...
DEBUG Downloading 20240224/cpython-3.9.18%2B20240224-aarch64-apple-darwin-pgo%2Blto-full.tar.zst to temporary location /Users/zb/Library/Application Support/uv/toolchains/.tmpgohKwp
DEBUG Extracting cpython-3.9.18%2B20240224-aarch64-apple-darwin-pgo%2Blto-full.tar.zst
DEBUG Moving /Users/zb/Library/Application Support/uv/toolchains/.tmpgohKwp/python to /Users/zb/Library/Application Support/uv/toolchains/cpython-3.9.18-macos-aarch64-none
Using Python 3.9.18 interpreter at: /Users/zb/Library/Application Support/uv/toolchains/cpython-3.9.18-macos-aarch64-none/install/bin/python3
Creating virtualenv at: .venv
INFO Removing existing directory
Activate with: source .venv/bin/activate
```

The preview flag is required. The fetch is performed if we can't find an
interpreter that satisfies the request. Once fetched, the toolchain will
be available for later invocations that include the `--preview` flag.
There will be follow-ups to improve toolchain management in general,
there is still outstanding work from the initial implementation.
2024-06-10 14:10:45 +00:00
Charlie Marsh
e7c573cfcb
Fix existing typos and enable typos in CI (#4184) 2024-06-10 01:50:54 +00:00
Zanie Blue
13f03e9d3f
Reduce uv-toolchain discovery API to Toolchain (#4148)
Drops `find_toolchain`, `find_best_toolchain`, etc. in favor of
`Toolchain::find_...`

We can change this in the future, but there should only be one "right"
way to do it not two redundant ways in the public interface.
2024-06-07 16:56:25 -05:00
Zanie Blue
53035d65a1
Refactor uv-toolchain types (#4121)
Extends #4120 
Part of #2607 

There should be no behavior changes here. Restructures the discovery API
to be focused on a toolchain first perspective in preparation for
exposing a `find_or_fetch` method for toolchains in
https://github.com/astral-sh/uv/pull/4138.
2024-06-07 14:20:28 -05:00
Zanie Blue
325982c418
Rename uv-interpreter crate to uv-toolchain (#4120)
In preparation for managed toolchains #2607, just renames the crate to
something broader.

See #4121 and https://github.com/astral-sh/uv/pull/4138 to see the final
intent.
2024-06-07 13:59:14 -05:00
Zanie Blue
630d3fde5c
Merge uv-toolchain and uv-interpreter (#3265)
Moves all of `uv-toolchain` into `uv-interpreter`. We may split these
out in the future, but the refactoring I want to do for interpreter
discovery is easier if I don't have to deal with entanglement. Includes
some restructuring of `uv-interpreter`.

Part of #2386
2024-04-30 17:49:46 +00:00
Zanie Blue
3b6e16bb0e
Fix uv-toolchain requirement on pep508_rs (#3256) 2024-04-24 19:10:47 -04:00
Andrew Gallant
67d8805ca8
uv-toolchain: use colocated temporary directory (#3240)
Previously, this would use the "system" temporary directory.
Because we rename the resulting directory to its final destination,
and because renaming is implemented via hardlinking, and because
hardlinking doesn't work across mount points, and because /tmp is
commonly on a different mount point than where `uv` is checked out,
we "fix" this by putting the temporary directory somewhere close to
the final destination of the fetched artifact.

There are alternatives we might consider pursuing. For example,
if the `rename` fails, then we should probably do a recursive
directory copy. But this is a quick fix for now and it also
consistent with colocation of other temporary directories in `uv`.

The main downside of this change is that if a user does ^C while
`uv-dev fetch-python` is running, then there is no mechanism for
cleaning up temporary directories.
2024-04-24 10:56:03 -04:00
konsti
3783292c43
Remove unused dependencies (#3236)
`cargo shear --fix` and some manual fixing for tokio and flate2.

I wanted to prepare my branch and realized main also needs this.
2024-04-24 11:18:24 +00:00
Charlie Marsh
7fb2bf816f
Add JSON Schema support (#3046)
## Summary

This PR adds JSON Schema support. The setup mirrors Ruff's own.
2024-04-17 17:24:41 +00:00
Charlie Marsh
295b58ad37
Add uv-workspace crate with settings discovery and deserialization (#3007)
## Summary

This PR adds basic struct definitions along with a "workspace" concept
for discovering settings. (The "workspace" terminology is used to match
Ruff; I did not invent it.)

A few notes:

- We discover any `pyproject.toml` or `uv.toml` file in any parent
directory of the current working directory. (We could adjust this to
look at the directories of the input files.)
- We don't actually do anything with the configuration yet; but those
PRs are large and I want this to be reviewed in isolation.
2024-04-16 13:56:47 -04:00
Zanie Blue
44e39bdca3
Replace Python bootstrapping script with Rust implementation (#2842)
See https://github.com/astral-sh/uv/issues/2617

Note this also includes:
- #2918 
- #2931 (pending)

A first step towards Python toolchain management in Rust.

First, we add a new crate to manage Python download metadata:

- Adds a new `uv-toolchain` crate
- Adds Rust structs for Python version download metadata
- Duplicates the script which downloads Python version metadata
- Adds a script to generate Rust code from the JSON metadata
- Adds a utility to download and extract the Python version

I explored some alternatives like a build script using things like
`serde` and `uneval` to automatically construct the code from our
structs but deemed it to heavy. Unlike Rye, I don't generate the Rust
directly from the web requests and have an intermediate JSON layer to
speed up iteration on the Rust types.

Next, we add add a `uv-dev` command `fetch-python` to download Python
versions per the bootstrapping script.

- Downloads a requested version or reads from `.python-versions`
- Extracts to `UV_BOOTSTRAP_DIR`
- Links executables for path extension

This command is not really intended to be user facing, but it's a good
PoC for the `uv-toolchain` API. Hash checking (via the sha256) isn't
implemented yet, we can do that in a follow-up.

Finally, we remove the `scripts/bootstrap` directory, update CI to use
the new command, and update the CONTRIBUTING docs.

<img width="1023" alt="Screenshot 2024-04-08 at 17 12 15"
src="57bd3cf1-7477-4bb8-a8e9-802a00d772cb">
2024-04-10 11:22:41 -05:00