Skip to content

Commit bc48840

Browse files
authored
Merge pull request #1819 from python-gitlab/jlvillal/encoded_id
fix: use url-encoded ID in all paths
2 parents 58e5b25 + b07eece commit bc48840

26 files changed

+267
-134
lines changed

gitlab/base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,15 @@ def get_id(self) -> Any:
217217
return None
218218
return getattr(self, self._id_attr)
219219

220+
@property
221+
def encoded_id(self) -> Any:
222+
"""Ensure that the ID is url-encoded so that it can be safely used in a URL
223+
path"""
224+
obj_id = self.get_id()
225+
if isinstance(obj_id, str):
226+
obj_id = gitlab.utils.EncodedId(obj_id)
227+
return obj_id
228+
220229
@property
221230
def attributes(self) -> Dict[str, Any]:
222231
d = self.__dict__["_updated_attrs"].copy()

gitlab/mixins.py

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,8 @@ def get(
9999
GitlabAuthenticationError: If authentication is not correct
100100
GitlabGetError: If the server cannot perform the request
101101
"""
102-
if not isinstance(id, int):
103-
id = utils._url_encode(id)
102+
if isinstance(id, str):
103+
id = utils.EncodedId(id)
104104
path = f"{self.path}/{id}"
105105
if TYPE_CHECKING:
106106
assert self._obj_cls is not None
@@ -173,7 +173,7 @@ def refresh(self, **kwargs: Any) -> None:
173173
GitlabGetError: If the server cannot perform the request
174174
"""
175175
if self._id_attr:
176-
path = f"{self.manager.path}/{self.id}"
176+
path = f"{self.manager.path}/{self.encoded_id}"
177177
else:
178178
if TYPE_CHECKING:
179179
assert self.manager.path is not None
@@ -391,7 +391,7 @@ def update(
391391
if id is None:
392392
path = self.path
393393
else:
394-
path = f"{self.path}/{id}"
394+
path = f"{self.path}/{utils.EncodedId(id)}"
395395

396396
self._check_missing_update_attrs(new_data)
397397
files = {}
@@ -444,7 +444,7 @@ def set(self, key: str, value: str, **kwargs: Any) -> base.RESTObject:
444444
Returns:
445445
The created/updated attribute
446446
"""
447-
path = f"{self.path}/{utils._url_encode(key)}"
447+
path = f"{self.path}/{utils.EncodedId(key)}"
448448
data = {"value": value}
449449
server_data = self.gitlab.http_put(path, post_data=data, **kwargs)
450450
if TYPE_CHECKING:
@@ -477,9 +477,7 @@ def delete(self, id: Union[str, int], **kwargs: Any) -> None:
477477
if id is None:
478478
path = self.path
479479
else:
480-
if not isinstance(id, int):
481-
id = utils._url_encode(id)
482-
path = f"{self.path}/{id}"
480+
path = f"{self.path}/{utils.EncodedId(id)}"
483481
self.gitlab.http_delete(path, **kwargs)
484482

485483

@@ -545,7 +543,7 @@ def save(self, **kwargs: Any) -> None:
545543
return
546544

547545
# call the manager
548-
obj_id = self.get_id()
546+
obj_id = self.encoded_id
549547
if TYPE_CHECKING:
550548
assert isinstance(self.manager, UpdateMixin)
551549
server_data = self.manager.update(obj_id, updated_data, **kwargs)
@@ -575,7 +573,7 @@ def delete(self, **kwargs: Any) -> None:
575573
"""
576574
if TYPE_CHECKING:
577575
assert isinstance(self.manager, DeleteMixin)
578-
self.manager.delete(self.get_id(), **kwargs)
576+
self.manager.delete(self.encoded_id, **kwargs)
579577

580578

581579
class UserAgentDetailMixin(_RestObjectBase):
@@ -598,7 +596,7 @@ def user_agent_detail(self, **kwargs: Any) -> Dict[str, Any]:
598596
GitlabAuthenticationError: If authentication is not correct
599597
GitlabGetError: If the server cannot perform the request
600598
"""
601-
path = f"{self.manager.path}/{self.get_id()}/user_agent_detail"
599+
path = f"{self.manager.path}/{self.encoded_id}/user_agent_detail"
602600
result = self.manager.gitlab.http_get(path, **kwargs)
603601
if TYPE_CHECKING:
604602
assert not isinstance(result, requests.Response)
@@ -631,7 +629,7 @@ def approve(
631629
GitlabUpdateError: If the server fails to perform the request
632630
"""
633631

634-
path = f"{self.manager.path}/{self.id}/approve"
632+
path = f"{self.manager.path}/{self.encoded_id}/approve"
635633
data = {"access_level": access_level}
636634
server_data = self.manager.gitlab.http_put(path, post_data=data, **kwargs)
637635
if TYPE_CHECKING:
@@ -705,7 +703,7 @@ def subscribe(self, **kwargs: Any) -> None:
705703
GitlabAuthenticationError: If authentication is not correct
706704
GitlabSubscribeError: If the subscription cannot be done
707705
"""
708-
path = f"{self.manager.path}/{self.get_id()}/subscribe"
706+
path = f"{self.manager.path}/{self.encoded_id}/subscribe"
709707
server_data = self.manager.gitlab.http_post(path, **kwargs)
710708
if TYPE_CHECKING:
711709
assert not isinstance(server_data, requests.Response)
@@ -725,7 +723,7 @@ def unsubscribe(self, **kwargs: Any) -> None:
725723
GitlabAuthenticationError: If authentication is not correct
726724
GitlabUnsubscribeError: If the unsubscription cannot be done
727725
"""
728-
path = f"{self.manager.path}/{self.get_id()}/unsubscribe"
726+
path = f"{self.manager.path}/{self.encoded_id}/unsubscribe"
729727
server_data = self.manager.gitlab.http_post(path, **kwargs)
730728
if TYPE_CHECKING:
731729
assert not isinstance(server_data, requests.Response)
@@ -752,7 +750,7 @@ def todo(self, **kwargs: Any) -> None:
752750
GitlabAuthenticationError: If authentication is not correct
753751
GitlabTodoError: If the todo cannot be set
754752
"""
755-
path = f"{self.manager.path}/{self.get_id()}/todo"
753+
path = f"{self.manager.path}/{self.encoded_id}/todo"
756754
self.manager.gitlab.http_post(path, **kwargs)
757755

758756

@@ -781,7 +779,7 @@ def time_stats(self, **kwargs: Any) -> Dict[str, Any]:
781779
if "time_stats" in self.attributes:
782780
return self.attributes["time_stats"]
783781

784-
path = f"{self.manager.path}/{self.get_id()}/time_stats"
782+
path = f"{self.manager.path}/{self.encoded_id}/time_stats"
785783
result = self.manager.gitlab.http_get(path, **kwargs)
786784
if TYPE_CHECKING:
787785
assert not isinstance(result, requests.Response)
@@ -800,7 +798,7 @@ def time_estimate(self, duration: str, **kwargs: Any) -> Dict[str, Any]:
800798
GitlabAuthenticationError: If authentication is not correct
801799
GitlabTimeTrackingError: If the time tracking update cannot be done
802800
"""
803-
path = f"{self.manager.path}/{self.get_id()}/time_estimate"
801+
path = f"{self.manager.path}/{self.encoded_id}/time_estimate"
804802
data = {"duration": duration}
805803
result = self.manager.gitlab.http_post(path, post_data=data, **kwargs)
806804
if TYPE_CHECKING:
@@ -819,7 +817,7 @@ def reset_time_estimate(self, **kwargs: Any) -> Dict[str, Any]:
819817
GitlabAuthenticationError: If authentication is not correct
820818
GitlabTimeTrackingError: If the time tracking update cannot be done
821819
"""
822-
path = f"{self.manager.path}/{self.get_id()}/reset_time_estimate"
820+
path = f"{self.manager.path}/{self.encoded_id}/reset_time_estimate"
823821
result = self.manager.gitlab.http_post(path, **kwargs)
824822
if TYPE_CHECKING:
825823
assert not isinstance(result, requests.Response)
@@ -838,7 +836,7 @@ def add_spent_time(self, duration: str, **kwargs: Any) -> Dict[str, Any]:
838836
GitlabAuthenticationError: If authentication is not correct
839837
GitlabTimeTrackingError: If the time tracking update cannot be done
840838
"""
841-
path = f"{self.manager.path}/{self.get_id()}/add_spent_time"
839+
path = f"{self.manager.path}/{self.encoded_id}/add_spent_time"
842840
data = {"duration": duration}
843841
result = self.manager.gitlab.http_post(path, post_data=data, **kwargs)
844842
if TYPE_CHECKING:
@@ -857,7 +855,7 @@ def reset_spent_time(self, **kwargs: Any) -> Dict[str, Any]:
857855
GitlabAuthenticationError: If authentication is not correct
858856
GitlabTimeTrackingError: If the time tracking update cannot be done
859857
"""
860-
path = f"{self.manager.path}/{self.get_id()}/reset_spent_time"
858+
path = f"{self.manager.path}/{self.encoded_id}/reset_spent_time"
861859
result = self.manager.gitlab.http_post(path, **kwargs)
862860
if TYPE_CHECKING:
863861
assert not isinstance(result, requests.Response)
@@ -893,7 +891,7 @@ def participants(self, **kwargs: Any) -> Dict[str, Any]:
893891
The list of participants
894892
"""
895893

896-
path = f"{self.manager.path}/{self.get_id()}/participants"
894+
path = f"{self.manager.path}/{self.encoded_id}/participants"
897895
result = self.manager.gitlab.http_get(path, **kwargs)
898896
if TYPE_CHECKING:
899897
assert not isinstance(result, requests.Response)
@@ -967,7 +965,7 @@ def promote(self, **kwargs: Any) -> Dict[str, Any]:
967965
The updated object data (*not* a RESTObject)
968966
"""
969967

970-
path = f"{self.manager.path}/{self.id}/promote"
968+
path = f"{self.manager.path}/{self.encoded_id}/promote"
971969
http_method = self._get_update_method()
972970
result = http_method(path, **kwargs)
973971
if TYPE_CHECKING:

gitlab/utils.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717

1818
import urllib.parse
19-
from typing import Any, Callable, Dict, Optional
19+
from typing import Any, Callable, Dict, Optional, Union
2020

2121
import requests
2222

@@ -56,25 +56,32 @@ def copy_dict(dest: Dict[str, Any], src: Dict[str, Any]) -> None:
5656
dest[k] = v
5757

5858

59-
def _url_encode(id: str) -> str:
60-
"""Encode/quote the characters in the string so that they can be used in a path.
59+
class EncodedId(str):
60+
"""A custom `str` class that will return the URL-encoded value of the string.
6161
62-
Reference to documentation on why this is necessary.
63-
64-
https://docs.gitlab.com/ee/api/index.html#namespaced-path-encoding
65-
66-
If using namespaced API requests, make sure that the NAMESPACE/PROJECT_PATH is
67-
URL-encoded. For example, / is represented by %2F
62+
* Using it recursively will only url-encode the value once.
63+
* Can accept either `str` or `int` as input value.
64+
* Can be used in an f-string and output the URL-encoded string.
6865
69-
https://docs.gitlab.com/ee/api/index.html#path-parameters
66+
Reference to documentation on why this is necessary.
7067
71-
Path parameters that are required to be URL-encoded must be followed. If not, it
72-
doesn’t match an API endpoint and responds with a 404. If there’s something in front
73-
of the API (for example, Apache), ensure that it doesn’t decode the URL-encoded path
74-
parameters.
68+
See::
7569
70+
https://docs.gitlab.com/ee/api/index.html#namespaced-path-encoding
71+
https://docs.gitlab.com/ee/api/index.html#path-parameters
7672
"""
77-
return urllib.parse.quote(id, safe="")
73+
74+
# mypy complains if return type other than the class type. So we ignore issue.
75+
def __new__( # type: ignore
76+
cls, value: Union[str, int, "EncodedId"]
77+
) -> Union[int, "EncodedId"]:
78+
if isinstance(value, (int, EncodedId)):
79+
return value
80+
81+
if not isinstance(value, str):
82+
raise TypeError(f"Unsupported type received: {type(value)}")
83+
value = urllib.parse.quote(value, safe="")
84+
return super().__new__(cls, value)
7885

7986

8087
def remove_none_from_dict(data: Dict[str, Any]) -> Dict[str, Any]:

gitlab/v4/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def _process_from_parent_attrs(self) -> None:
7575
if key not in self.args:
7676
continue
7777

78-
self.parent_args[key] = gitlab.utils._url_encode(self.args[key])
78+
self.parent_args[key] = gitlab.utils.EncodedId(self.args[key])
7979
# If we don't delete it then it will be added to the URL as a query-string
8080
del self.args[key]
8181

gitlab/v4/objects/commits.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def diff(self, **kwargs: Any) -> Union[gitlab.GitlabList, List[Dict[str, Any]]]:
4242
Returns:
4343
The changes done in this commit
4444
"""
45-
path = f"{self.manager.path}/{self.get_id()}/diff"
45+
path = f"{self.manager.path}/{self.encoded_id}/diff"
4646
return self.manager.gitlab.http_list(path, **kwargs)
4747

4848
@cli.register_custom_action("ProjectCommit", ("branch",))
@@ -58,7 +58,7 @@ def cherry_pick(self, branch: str, **kwargs: Any) -> None:
5858
GitlabAuthenticationError: If authentication is not correct
5959
GitlabCherryPickError: If the cherry-pick could not be performed
6060
"""
61-
path = f"{self.manager.path}/{self.get_id()}/cherry_pick"
61+
path = f"{self.manager.path}/{self.encoded_id}/cherry_pick"
6262
post_data = {"branch": branch}
6363
self.manager.gitlab.http_post(path, post_data=post_data, **kwargs)
6464

@@ -80,7 +80,7 @@ def refs(
8080
Returns:
8181
The references the commit is pushed to.
8282
"""
83-
path = f"{self.manager.path}/{self.get_id()}/refs"
83+
path = f"{self.manager.path}/{self.encoded_id}/refs"
8484
query_data = {"type": type}
8585
return self.manager.gitlab.http_list(path, query_data=query_data, **kwargs)
8686

@@ -101,7 +101,7 @@ def merge_requests(
101101
Returns:
102102
The merge requests related to the commit.
103103
"""
104-
path = f"{self.manager.path}/{self.get_id()}/merge_requests"
104+
path = f"{self.manager.path}/{self.encoded_id}/merge_requests"
105105
return self.manager.gitlab.http_list(path, **kwargs)
106106

107107
@cli.register_custom_action("ProjectCommit", ("branch",))
@@ -122,7 +122,7 @@ def revert(
122122
Returns:
123123
The new commit data (*not* a RESTObject)
124124
"""
125-
path = f"{self.manager.path}/{self.get_id()}/revert"
125+
path = f"{self.manager.path}/{self.encoded_id}/revert"
126126
post_data = {"branch": branch}
127127
return self.manager.gitlab.http_post(path, post_data=post_data, **kwargs)
128128

@@ -141,7 +141,7 @@ def signature(self, **kwargs: Any) -> Union[Dict[str, Any], requests.Response]:
141141
Returns:
142142
The commit's signature data
143143
"""
144-
path = f"{self.manager.path}/{self.get_id()}/signature"
144+
path = f"{self.manager.path}/{self.encoded_id}/signature"
145145
return self.manager.gitlab.http_get(path, **kwargs)
146146

147147

gitlab/v4/objects/environments.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def stop(self, **kwargs: Any) -> Union[Dict[str, Any], requests.Response]:
3636
Returns:
3737
A dict of the result.
3838
"""
39-
path = f"{self.manager.path}/{self.get_id()}/stop"
39+
path = f"{self.manager.path}/{self.encoded_id}/stop"
4040
return self.manager.gitlab.http_post(path, **kwargs)
4141

4242

gitlab/v4/objects/epics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def save(self, **kwargs: Any) -> None:
7272
return
7373

7474
# call the manager
75-
obj_id = self.get_id()
75+
obj_id = self.encoded_id
7676
self.manager.update(obj_id, updated_data, **kwargs)
7777

7878

gitlab/v4/objects/features.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def set(
5252
Returns:
5353
The created/updated attribute
5454
"""
55-
name = utils._url_encode(name)
55+
name = utils.EncodedId(name)
5656
path = f"{self.path}/{name}"
5757
data = {
5858
"value": value,

gitlab/v4/objects/files.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def save( # type: ignore
5656
"""
5757
self.branch = branch
5858
self.commit_message = commit_message
59-
self.file_path = utils._url_encode(self.file_path)
59+
self.file_path = utils.EncodedId(self.file_path)
6060
super(ProjectFile, self).save(**kwargs)
6161

6262
@exc.on_http_error(exc.GitlabDeleteError)
@@ -76,7 +76,7 @@ def delete( # type: ignore
7676
GitlabAuthenticationError: If authentication is not correct
7777
GitlabDeleteError: If the server cannot perform the request
7878
"""
79-
file_path = utils._url_encode(self.get_id())
79+
file_path = self.encoded_id
8080
self.manager.delete(file_path, branch, commit_message, **kwargs)
8181

8282

@@ -144,7 +144,7 @@ def create(
144144
assert data is not None
145145
self._check_missing_create_attrs(data)
146146
new_data = data.copy()
147-
file_path = utils._url_encode(new_data.pop("file_path"))
147+
file_path = utils.EncodedId(new_data.pop("file_path"))
148148
path = f"{self.path}/{file_path}"
149149
server_data = self.gitlab.http_post(path, post_data=new_data, **kwargs)
150150
if TYPE_CHECKING:
@@ -173,7 +173,7 @@ def update( # type: ignore
173173
"""
174174
new_data = new_data or {}
175175
data = new_data.copy()
176-
file_path = utils._url_encode(file_path)
176+
file_path = utils.EncodedId(file_path)
177177
data["file_path"] = file_path
178178
path = f"{self.path}/{file_path}"
179179
self._check_missing_update_attrs(data)
@@ -203,7 +203,7 @@ def delete( # type: ignore
203203
GitlabAuthenticationError: If authentication is not correct
204204
GitlabDeleteError: If the server cannot perform the request
205205
"""
206-
file_path = utils._url_encode(file_path)
206+
file_path = utils.EncodedId(file_path)
207207
path = f"{self.path}/{file_path}"
208208
data = {"branch": branch, "commit_message": commit_message}
209209
self.gitlab.http_delete(path, query_data=data, **kwargs)
@@ -239,7 +239,7 @@ def raw(
239239
Returns:
240240
The file content
241241
"""
242-
file_path = utils._url_encode(file_path)
242+
file_path = utils.EncodedId(file_path)
243243
path = f"{self.path}/{file_path}/raw"
244244
query_data = {"ref": ref}
245245
result = self.gitlab.http_get(
@@ -266,7 +266,7 @@ def blame(self, file_path: str, ref: str, **kwargs: Any) -> List[Dict[str, Any]]
266266
Returns:
267267
A list of commits/lines matching the file
268268
"""
269-
file_path = utils._url_encode(file_path)
269+
file_path = utils.EncodedId(file_path)
270270
path = f"{self.path}/{file_path}/blame"
271271
query_data = {"ref": ref}
272272
result = self.gitlab.http_list(path, query_data, **kwargs)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy