@@ -56,17 +56,77 @@ 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
+ * 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
+
59
119
@overload
60
120
def _url_encode (id : int ) -> int :
61
121
...
62
122
63
123
64
124
@overload
65
- def _url_encode (id : str ) -> str :
125
+ def _url_encode (id : Union [ str , EncodedId ] ) -> EncodedId :
66
126
...
67
127
68
128
69
- def _url_encode (id : Union [int , str ]) -> Union [int , str ]:
129
+ def _url_encode (id : Union [int , str , EncodedId ]) -> Union [int , EncodedId ]:
70
130
"""Encode/quote the characters in the string so that they can be used in a path.
71
131
72
132
Reference to documentation on why this is necessary.
@@ -84,9 +144,9 @@ def _url_encode(id: Union[int, str]) -> Union[int, str]:
84
144
parameters.
85
145
86
146
"""
87
- if isinstance (id , int ):
147
+ if isinstance (id , ( int , EncodedId ) ):
88
148
return id
89
- return urllib . parse . quote (id , safe = "" )
149
+ return EncodedId (id )
90
150
91
151
92
152
def remove_none_from_dict (data : Dict [str , Any ]) -> Dict [str , Any ]:
0 commit comments