@@ -56,17 +56,83 @@ 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+ # `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+
59125@overload
60126def _url_encode (id : int ) -> int :
61127 ...
62128
63129
64130@overload
65- def _url_encode (id : str ) -> str :
131+ def _url_encode (id : Union [ str , EncodedId ] ) -> EncodedId :
66132 ...
67133
68134
69- def _url_encode (id : Union [int , str ]) -> Union [int , str ]:
135+ def _url_encode (id : Union [int , str , EncodedId ]) -> Union [int , EncodedId ]:
70136 """Encode/quote the characters in the string so that they can be used in a path.
71137
72138 Reference to documentation on why this is necessary.
@@ -84,9 +150,9 @@ def _url_encode(id: Union[int, str]) -> Union[int, str]:
84150 parameters.
85151
86152 """
87- if isinstance (id , int ):
153+ if isinstance (id , ( int , EncodedId ) ):
88154 return id
89- return urllib . parse . quote (id , safe = "" )
155+ return EncodedId (id )
90156
91157
92158def remove_none_from_dict (data : Dict [str , Any ]) -> Dict [str , Any ]:
0 commit comments