feat(functions): refactor sync and async methods - #1338
Conversation
this way, we only need to write the whole client once, and we can derive both IO implementations from a single source of truth, reducing the amount of code needed for each package in half.
ee7748e to
49f0014
Compare
Pull Request Test Coverage Report for Build 21220319510Details
💛 - Coveralls |
lets start already very strict, so that we don't have to strict it up later.
this is because, inside the `http_endpoint` class definition, we have the equivalent of the following assignment: ``` s : ServerEndpoint[Success, Failure] a : ServerEndpoint[Awaitable[Success] | Success, Failure] = s ``` as such, we need to make `Awaitable[Success] | Success` a subtype of `Success`, which means that it needs to be covariant!
I'm currently experimenting with other type checkers, and seeing if they agree on the type stuff done in http. pyrefly accepts all of it, but complains about the TypeAlias annotation in the json part. it isnt necessary so I just removed it.
… TypedDicts this is a partial commit, and is not fully working yet.
this ideally makes it more general, and lets endpoints specify how they want to send information through in multiple ways. this is needed to handle the case where we send bytes through edge functions directly addionally, we're not using json.loads anymore, and relying solely on pydantic's `to_json` machinery, which should be miles faster (written in rust!)
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the
✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
the type ignore is useful for python < 3.11, but mypy doesnt know that
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In `@src/functions/src/supabase_functions/client.py`:
- Around line 220-230: create_client currently passes verify as the third
positional argument to AsyncFunctionsClient/SyncFunctionsClient, but those
constructors treat the third positional parameter as timeout; change the calls
in create_client so verify is passed as a named keyword (e.g., verify=verify) or
use the correct parameter ordering expected by
AsyncFunctionsClient/SyncFunctionsClient; update both return paths in
create_client to call AsyncFunctionsClient(url, headers, verify=verify) and
SyncFunctionsClient(url, headers, verify=verify).
In `@src/functions/src/supabase_functions/errors.py`:
- Around line 38-42: In on_error_response, normalize the x-relay-header value
before comparing so casing/whitespace don't misclassify relay errors: retrieve
response.headers.get("x-relay-header"), guard for None, and compare its
stripped, lowercased form to "true" (use that normalized value in the
is_relay_error check) so FunctionsRelayError is returned correctly when the
header contains variants like " True " or "TRUE".
In `@src/functions/tests/_async/test_function_client.py`:
- Around line 58-71: In test_invoke_success_json update the mocked httpx
Response to mirror real behavior by setting mock_response.content to bytes
(e.g., b'{"message": "success"}') instead of a string; locate the mock_response
object in the test_invoke_success_json function and change its content
assignment, and make the same change in the test_invoke_with_region test so both
mocks use bytes for Response.content when exercising AsyncFunctionsClient.invoke
via mock_request (AsyncMock).
In `@src/functions/tests/_sync/test_function_client.py`:
- Around line 58-71: The mock Response in test_invoke_success_json uses a string
for mock_response.content but should use bytes to mirror httpx.Response; update
mock_response.content to b'{"message": "success"}' (and make the same change in
test_invoke_with_region) so client.invoke receives bytes like a real response.
In `@src/utils/Makefile`:
- Around line 1-23: The Makefile is missing .PHONY declarations and the
conventional all/test targets which causes checkmake warnings; add a .PHONY line
listing help tests mypy clean build all test and create an all target that
depends on help (or your intended default) and a test target that depends on
tests (or runs the same commands as tests), ensuring the existing targets (help,
tests, mypy, clean, build) remain unchanged and referenced by these aliases so
Make treats them as phony rather than file names.
In `@src/utils/pyproject.toml`:
- Around line 24-28: The documentation URL under [project.urls] is currently
non-browsable; update the documentation value so it points to the browsable
GitHub tree (use /tree/main/) — i.e., replace the existing documentation =
"https://github.com/supabase/supabase-py/src/utils" with a proper tree path such
as documentation = "https://github.com/supabase/supabase-py/tree/main/src/utils"
(or the correct docs directory) so the documentation link is valid; edit the
documentation key in the [project.urls] section to make this change.
🧹 Nitpick comments (1)
src/functions/src/supabase_functions/client.py (1)
75-108: Unusedresponse_typeparameter.The
response_typeparameter is declared but never used in_invoke_options_to_request. Either implement the response type handling or remove the parameter to avoid confusion.
| [project.urls] | ||
| homepage = "https://github.com/supabase/supabase-py" | ||
| repository = "https://github.com/supabase/supabase-py" | ||
| documentation = "https://github.com/supabase/supabase-py/src/utils" | ||
| changelog = "https://github.com/supabase/supabase-py/tree/main/CHANGELOG.md" |
There was a problem hiding this comment.
Fix the documentation URL path (currently non-browsable).
GitHub expects /tree/main/ for repository paths; the current value likely 404s.
🔗 Suggested fix
-documentation = "https://github.com/supabase/supabase-py/src/utils"
+documentation = "https://github.com/supabase/supabase-py/tree/main/src/utils"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| [project.urls] | |
| homepage = "https://github.com/supabase/supabase-py" | |
| repository = "https://github.com/supabase/supabase-py" | |
| documentation = "https://github.com/supabase/supabase-py/src/utils" | |
| changelog = "https://github.com/supabase/supabase-py/tree/main/CHANGELOG.md" | |
| [project.urls] | |
| homepage = "https://github.com/supabase/supabase-py" | |
| repository = "https://github.com/supabase/supabase-py" | |
| documentation = "https://github.com/supabase/supabase-py/tree/main/src/utils" | |
| changelog = "https://github.com/supabase/supabase-py/tree/main/CHANGELOG.md" |
🤖 Prompt for AI Agents
In `@src/utils/pyproject.toml` around lines 24 - 28, The documentation URL under
[project.urls] is currently non-browsable; update the documentation value so it
points to the browsable GitHub tree (use /tree/main/) — i.e., replace the
existing documentation = "https://github.com/supabase/supabase-py/src/utils"
with a proper tree path such as documentation =
"https://github.com/supabase/supabase-py/tree/main/src/utils" (or the correct
docs directory) so the documentation link is valid; edit the documentation key
in the [project.urls] section to make this change.
rename classes with `endpoint` in their name. given that this is a client, and not a server, these might not make sense the initial idea was that these were supposed to hit a 'server endpoint', but I think that the name is confusing after trying to use it elsewhere i've also split the one request class into multiple, because it makes more sense in the context of the other packages
What kind of change does this PR introduce?
Preview for the sync vs async rewrite proposal, using
functionsas the first example. The idea for this rewrite is that we can refactor the methods in such a way to only write the core logic once, and derive both sync and async implementations out of the same client.To understand the rewrite, we should study the following hypothetical method, and its async counterpart:
We see that the only differences between the sync and async implementation is the
asyncandawaitkeywords. In this PR, I propose to rewrite these two into a single implementation, by removing the IO out of the methods:With this, we can write the
list_somethingmethod only once, as a description for how to send a request and how to parse a response, and execute it both ways, either sync or async.However, in order to execute it in this way, we'd need to externally call the client method
list_somethingin the executor:This would work, but would be cumbersome and tiring to use. Instead, we can a store an executor inside the client, and use a decorator to blindly call
communicateon the endpoint. Regardless of which kind of IO, this will return the correct object, and will work correctly.In order to derive the sync and async clients, all we'd need to do is pass the executor we want:
However, how do we convince mypy that this is the case?
Typing
In order to show mypy that the type of
list_something()depends on the executor kind, we need to add the executor as a generic type parameter ofSupabaseClient, and use@overloadson thehttp_endpointto decide whether it should be sync or async:Even though this seems complex, its not. Because the typing is not easy to write, I rewrote the
http_endpointdecorator to be a dataclass class, so that I could annotate it more easily. Thus, you can interpret it in the following way:Success | Awaitable[Sucess]SyncExecutor, the return is the sync version,SuccessAsyncExecutor, the return is the sync version,Awaitable[Sucess]The
ParamSpecpart is used to be generic over all the possible args and kwargs of any method passed in. Similarly, theHasExecutorprotocol is used to be generic over all the possible clients that can be passed in, so that we can share this decorator over multiple clients -- eg. SupabaseVectorClient and GoTrueClient.With this, mypy is satisfied, and
http_endpointtype checks for both the sync and async clients. Using the functions example in this PR:[In fact, this is a form of Higher Kinded Types in Python!
HasExecutoris a family of functor types. If you're interested, you can read more here: https://sobolevn.me/2020/10/higher-kinded-types-in-python].Summary by CodeRabbit
Refactor
Chores
✏️ Tip: You can customize this high-level summary in your review settings.