@@ -56,17 +56,77 @@ 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+ * 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.
65+
66+ Reference to documentation on why this is necessary.
67+
68+ See::
69+
70+ https://docs.gitlab.com/ee/api/index.html#namespaced-path-encoding
71+ https://docs.gitlab.com/ee/api/index.html#path-parameters
72+ """
73+
74+ # `original_str` will contain the original string value that was used to create the
75+ # first instance of EncodedId. We will use this original value to generate the
76+ # URL-encoded value each time.
77+ original_str : str
78+
79+ def __new__ (cls , value : Union [str , int , "EncodedId" ]) -> "EncodedId" :
80+ # __new__() gets called before __init__()
81+ if isinstance (value , int ):
82+ value = str (value )
83+ # Make sure isinstance() for `EncodedId` comes before check for `str` as
84+ # `EncodedId` is an instance of `str` and would pass that check.
85+ elif isinstance (value , EncodedId ):
86+ # We use the original string value to URL-encode
87+ value = value .original_str
88+ elif isinstance (value , str ):
89+ pass
90+ else :
91+ raise ValueError (f"Unsupported type received: { type (value )} " )
92+ # Set the value our string will return
93+ value = urllib .parse .quote (value , safe = "" )
94+ return super ().__new__ (cls , value )
95+
96+ def __init__ (self , value : Union [int , str ]) -> None :
97+ # At this point `super().__str__()` returns the URL-encoded value. Which means
98+ # when using this as a `str` it will return the URL-encoded value.
99+ #
100+ # But `value` contains the original value passed in `EncodedId(value)`. We use
101+ # this to always keep the original string that was received so that no matter
102+ # how many times we recurse we only URL-encode our original string once.
103+ if isinstance (value , int ):
104+ value = str (value )
105+ # Make sure isinstance() for `EncodedId` comes before check for `str` as
106+ # `EncodedId` is an instance of `str` and would pass that check.
107+ elif isinstance (value , EncodedId ):
108+ # This is the key part as we are always keeping the original string even
109+ # through multiple recursions.
110+ value = value .original_str
111+ elif isinstance (value , str ):
112+ pass
113+ else :
114+ raise ValueError (f"Unsupported type received: { type (value )} " )
115+ self .original_str = value
116+ super ().__init__ ()
117+
118+
59119@overload
60120def _url_encode (id : int ) -> int :
61121 ...
62122
63123
64124@overload
65- def _url_encode (id : str ) -> str :
125+ def _url_encode (id : Union [ str , EncodedId ] ) -> EncodedId :
66126 ...
67127
68128
69- def _url_encode (id : Union [int , str ]) -> Union [int , str ]:
129+ def _url_encode (id : Union [int , str , EncodedId ]) -> Union [int , EncodedId ]:
70130 """Encode/quote the characters in the string so that they can be used in a path.
71131
72132 Reference to documentation on why this is necessary.
@@ -84,9 +144,9 @@ def _url_encode(id: Union[int, str]) -> Union[int, str]:
84144 parameters.
85145
86146 """
87- if isinstance (id , int ):
147+ if isinstance (id , ( int , EncodedId ) ):
88148 return id
89- return urllib . parse . quote (id , safe = "" )
149+ return EncodedId (id )
90150
91151
92152def remove_none_from_dict (data : Dict [str , Any ]) -> Dict [str , Any ]:
0 commit comments