-
Notifications
You must be signed in to change notification settings - Fork 2
fix: percent-encode storage resource paths on the wire (#124) #125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development
Are you sure you want to change the base?
Changes from all commits
552f842
c6b6848
3a63b3f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| from pathlib import PurePosixPath | ||
| from typing import Literal, cast, get_args | ||
| from urllib.parse import urljoin, urlparse | ||
| from urllib.parse import quote, unquote, urljoin, urlparse, urlsplit | ||
|
|
||
| from aidial_client._compatibility.pydantic_v1 import BaseModel | ||
| from aidial_client._constants import API_PREFIX | ||
|
|
@@ -12,6 +12,22 @@ | |
| StorageResourceType = Literal["files", "conversations", "prompts"] | ||
|
|
||
|
|
||
| def percent_encode_resource_url(url: str) -> str: | ||
| """ | ||
| Percent-encode each path segment so reserved characters (space, ``#``, | ||
| ``?``, ``[`` …) reach DIAL Core encoded instead of making it answer 500. | ||
| Segments are decoded first, so a decoded path (``my file.txt``) and an | ||
| already-encoded one (``my%20file.txt``, as returned by the API) converge | ||
| without double-encoding. Absolute URLs come from the API already encoded and | ||
| are returned untouched. | ||
| """ | ||
| if urlsplit(url).netloc: | ||
| return url | ||
|
|
||
| segments = url.split("/") | ||
| return "/".join(quote(unquote(seg), safe="") for seg in segments) | ||
|
|
||
|
|
||
| def _is_directory(s: str) -> bool: | ||
| return s[-1] == "/" | ||
|
|
||
|
|
@@ -153,6 +169,15 @@ def get_api_path(self, url: str) -> str: | |
| """ | ||
| return self.get_storage_resource(url).api_path | ||
|
|
||
| def get_encoded_api_path(self, url: str) -> str: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's extend the type of the argument to |
||
| """ | ||
| Relative api path with every segment percent-encoded for the wire. | ||
|
|
||
| Encodes before parsing so reserved characters (notably ``#`` and ``?``, | ||
| which ``urlparse`` would otherwise drop as fragment/query) survive. | ||
| """ | ||
| return self.get_api_path(percent_encode_resource_url(url)) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's instead move the encoding to the very place where url is converted into the resource: |
||
|
|
||
| def get_display_name(self, url: str) -> str: | ||
| """ | ||
| Get the display name of the resource from the URL | ||
|
|
@@ -164,7 +189,9 @@ def _prepare_download_request( | |
| url: str | PurePosixPath, | ||
| etag_if_match: str | None, | ||
| ) -> tuple[FinalRequestOptions, str]: | ||
| storage_resource = self.get_storage_resource(str(url)) | ||
| storage_resource = self.get_storage_resource( | ||
| percent_encode_resource_url(str(url)) | ||
| ) | ||
|
|
||
| if storage_resource.filename is None: | ||
| raise InvalidDialURLError("URL points to a directory, not a file") | ||
|
|
@@ -179,4 +206,5 @@ def _prepare_download_request( | |
| ), | ||
| ) | ||
|
|
||
| return options, storage_resource.filename | ||
| # api_path is percent-encoded; return a human-readable filename. | ||
| return options, unquote(storage_resource.filename) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,8 @@ | ||
| from aidial_client._internal_types._generic import NoneType | ||
| from aidial_client._internal_types._http_request import FinalRequestOptions | ||
| from aidial_client.helpers.storage_resource import ( | ||
| percent_encode_resource_url, | ||
| ) | ||
| from aidial_client.resources.base import AsyncResource, Resource | ||
|
|
||
| _GRANT_URL = "v1/ops/resource/per-request-permissions/grant" | ||
|
|
@@ -21,7 +24,10 @@ def grant( | |
| url=_GRANT_URL, | ||
| json_data={ | ||
| "resourcePermissions": [ | ||
| {"url": url, "permissions": permissions} | ||
| { | ||
| "url": percent_encode_resource_url(url), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's extract this into a Reuse in sync and async versions. And use |
||
| "permissions": permissions, | ||
| } | ||
| for url in resources | ||
| ], | ||
| "receiver": receiver, | ||
|
|
@@ -46,7 +52,10 @@ async def grant( | |
| url=_GRANT_URL, | ||
| json_data={ | ||
| "resourcePermissions": [ | ||
| {"url": url, "permissions": permissions} | ||
| { | ||
| "url": percent_encode_resource_url(url), | ||
| "permissions": permissions, | ||
| } | ||
| for url in resources | ||
| ], | ||
| "receiver": receiver, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's rename it to
_percent_encode_relative_urlsince there is nothing in the implementation of the function that's specific to DIAL resources, and I don't see why we need to make it public.