@@ -56,17 +56,88 @@ def copy_dict(dest: Dict[str, Any], src: Dict[str, Any]) -> None:
5656 dest [k ] = v
5757
5858
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+
59130@overload
60131def _url_encode (id : int ) -> int :
61132 ...
62133
63134
64135@overload
65- def _url_encode (id : str ) -> str :
136+ def _url_encode (id : Union [ str , EncodedId ] ) -> EncodedId :
66137 ...
67138
68139
69- def _url_encode (id : Union [int , str ]) -> Union [int , str ]:
140+ def _url_encode (id : Union [int , str , EncodedId ]) -> Union [int , EncodedId ]:
70141 """Encode/quote the characters in the string so that they can be used in a path.
71142
72143 Reference to documentation on why this is necessary.
@@ -84,9 +155,9 @@ def _url_encode(id: Union[int, str]) -> Union[int, str]:
84155 parameters.
85156
86157 """
87- if isinstance (id , int ):
158+ if isinstance (id , ( int , EncodedId ) ):
88159 return id
89- return urllib . parse . quote (id , safe = "" )
160+ return EncodedId (id )
90161
91162
92163def remove_none_from_dict (data : Dict [str , Any ]) -> Dict [str , Any ]:
0 commit comments