@@ -56,17 +56,88 @@ def copy_dict(dest: Dict[str, Any], src: Dict[str, Any]) -> None:
56
56
dest [k ] = v
57
57
58
58
59
+ class EncodedId (str ):
60
+ """A custom `str` class that will return the URL-encoded value of the string.
61
+
62
+ Features:
63
+ * Using it recursively will only url-encode the value once.
64
+ * Can accept either `str` or `int` as input value.
65
+ * Can be used in an f-string and output the URL-encoded string.
66
+
67
+ Reference to documentation on why this is necessary.
68
+
69
+ https://docs.gitlab.com/ee/api/index.html#namespaced-path-encoding
70
+
71
+ If using namespaced API requests, make sure that the NAMESPACE/PROJECT_PATH is
72
+ URL-encoded. For example, / is represented by %2F
73
+
74
+ https://docs.gitlab.com/ee/api/index.html#path-parameters
75
+
76
+ Path parameters that are required to be URL-encoded must be followed. If not, it
77
+ doesn’t match an API endpoint and responds with a 404. If there’s something in
78
+ front of the API (for example, Apache), ensure that it doesn’t decode the
79
+ URL-encoded path parameters.
80
+
81
+
82
+ When creating an EncodedId instance `__new__` will be called first and then
83
+ `__init__`.
84
+ """
85
+
86
+ # `original_str` will contain the original string value that was used to create the
87
+ # first instance of EncodedId. We will use this original value to generate the
88
+ # URL-encoded value each time.
89
+ original_str : str
90
+
91
+ def __new__ (cls , value : Union [str , int , "EncodedId" ]) -> "EncodedId" :
92
+ if isinstance (value , int ):
93
+ value = str (value )
94
+ # Make sure isinstance() for `EncodedId` comes before check for `str` as
95
+ # `EncodedId` is an instance of `str` and would pass that check.
96
+ elif isinstance (value , EncodedId ):
97
+ # We use the original string value to URL-encode
98
+ value = value .original_str
99
+ elif isinstance (value , str ):
100
+ pass
101
+ else :
102
+ raise ValueError (f"Unsupported type received: { type (value )} " )
103
+ # Set the value our string will return
104
+ value = urllib .parse .quote (value , safe = "" )
105
+ return super ().__new__ (cls , value )
106
+
107
+ def __init__ (self , value : Union [int , str ]) -> None :
108
+ # At this point `super().__str__()` returns the URL-encoded value. Which means
109
+ # when using this as a `str` it will return the URL-encoded value.
110
+ #
111
+ # But `value` contains the original value passed in `EncodedId(value)`. We use
112
+ # this to always keep the original string that was received so that no matter
113
+ # how many times we recurse we only URL-encode our original string once.
114
+ if isinstance (value , int ):
115
+ value = str (value )
116
+ # Make sure isinstance() for `EncodedId` comes before check for `str` as
117
+ # `EncodedId` is an instance of `str` and would pass that check.
118
+ elif isinstance (value , EncodedId ):
119
+ # This is the key part as we are always keeping the original string even
120
+ # through multiple recursions.
121
+ value = value .original_str
122
+ elif isinstance (value , str ):
123
+ pass
124
+ else :
125
+ raise ValueError (f"Unsupported type received: { type (value )} " )
126
+ self .original_str = value
127
+ super ().__init__ ()
128
+
129
+
59
130
@overload
60
131
def _url_encode (id : int ) -> int :
61
132
...
62
133
63
134
64
135
@overload
65
- def _url_encode (id : str ) -> str :
136
+ def _url_encode (id : Union [ str , EncodedId ] ) -> EncodedId :
66
137
...
67
138
68
139
69
- def _url_encode (id : Union [int , str ]) -> Union [int , str ]:
140
+ def _url_encode (id : Union [int , str , EncodedId ]) -> Union [int , EncodedId ]:
70
141
"""Encode/quote the characters in the string so that they can be used in a path.
71
142
72
143
Reference to documentation on why this is necessary.
@@ -84,9 +155,9 @@ def _url_encode(id: Union[int, str]) -> Union[int, str]:
84
155
parameters.
85
156
86
157
"""
87
- if isinstance (id , int ):
158
+ if isinstance (id , ( int , EncodedId ) ):
88
159
return id
89
- return urllib . parse . quote (id , safe = "" )
160
+ return EncodedId (id )
90
161
91
162
92
163
def remove_none_from_dict (data : Dict [str , Any ]) -> Dict [str , Any ]:
0 commit comments