@@ -56,17 +56,83 @@ 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
+ # `original_str` will contain the original string value that was used to create the
82
+ # first instance of EncodedId. We will use this original value to generate the
83
+ # URL-encoded value each time.
84
+ original_str : str
85
+
86
+ def __init__ (self , value : Union [int , str ]) -> None :
87
+ # At this point `super().__str__()` returns the URL-encoded value. Which means
88
+ # when using this as a `str` it will return the URL-encoded value.
89
+ #
90
+ # But `value` contains the original value passed in `EncodedId(value)`. We use
91
+ # this to always keep the original string that was received so that no matter
92
+ # how many times we recurse we only URL-encode our original string once.
93
+ if isinstance (value , int ):
94
+ value = str (value )
95
+ # Make sure isinstance() for `EncodedId` comes before check for `str` as
96
+ # `EncodedId` is an instance of `str` and would pass that check.
97
+ elif isinstance (value , EncodedId ):
98
+ # This is the key part as we are always keeping the original string even
99
+ # through multiple recursions.
100
+ value = value .original_str
101
+ elif isinstance (value , str ):
102
+ pass
103
+ else :
104
+ raise ValueError (f"Unsupported type received: { type (value )} " )
105
+ self .original_str = value
106
+ super ().__init__ ()
107
+
108
+ def __new__ (cls , value : Union [str , int , "EncodedId" ]) -> "EncodedId" :
109
+ if isinstance (value , int ):
110
+ value = str (value )
111
+ # Make sure isinstance() for `EncodedId` comes before check for `str` as
112
+ # `EncodedId` is an instance of `str` and would pass that check.
113
+ elif isinstance (value , EncodedId ):
114
+ # We use the original string value to URL-encode
115
+ value = value .original_str
116
+ elif isinstance (value , str ):
117
+ pass
118
+ else :
119
+ raise ValueError (f"Unsupported type received: { type (value )} " )
120
+ # Set the value our string will return
121
+ value = urllib .parse .quote (value , safe = "" )
122
+ return super ().__new__ (cls , value )
123
+
124
+
59
125
@overload
60
126
def _url_encode (id : int ) -> int :
61
127
...
62
128
63
129
64
130
@overload
65
- def _url_encode (id : str ) -> str :
131
+ def _url_encode (id : Union [ str , EncodedId ] ) -> EncodedId :
66
132
...
67
133
68
134
69
- def _url_encode (id : Union [int , str ]) -> Union [int , str ]:
135
+ def _url_encode (id : Union [int , str , EncodedId ]) -> Union [int , EncodedId ]:
70
136
"""Encode/quote the characters in the string so that they can be used in a path.
71
137
72
138
Reference to documentation on why this is necessary.
@@ -84,9 +150,9 @@ def _url_encode(id: Union[int, str]) -> Union[int, str]:
84
150
parameters.
85
151
86
152
"""
87
- if isinstance (id , int ):
153
+ if isinstance (id , ( int , EncodedId ) ):
88
154
return id
89
- return urllib . parse . quote (id , safe = "" )
155
+ return EncodedId (id )
90
156
91
157
92
158
def remove_none_from_dict (data : Dict [str , Any ]) -> Dict [str , Any ]:
0 commit comments