1919#
2020
2121import re
22+ from typing import Dict , List , Optional , TypedDict , Union
23+ from typing_extensions import Required , NotRequired
24+
2225
2326# Matches on a string specifying memory size
2427MEM = re .compile ('^[1-9][0-9]*[MGTP]$' )
5255 "spaces" ,
5356 "virt_type" ,
5457 "zones" ,
55- "allocate_public_ip" ]
58+ "allocate_public_ip" ,
59+ ]
5660
5761LIST_KEYS = {'tags' , 'spaces' , 'zones' }
5862
5963SNAKE1 = re .compile (r'(.)([A-Z][a-z]+)' )
6064SNAKE2 = re .compile ('([a-z0-9])([A-Z])' )
6165
6266
63- def parse (constraints ):
67+ ParsedValue = Union [int , bool , str ]
68+
69+
70+ class ConstraintsDict (TypedDict , total = False ):
71+ allocate_public_ip : ParsedValue
72+ arch : ParsedValue
73+ container : ParsedValue
74+ cores : ParsedValue
75+ cpu_cores : ParsedValue
76+ cpu_power : ParsedValue
77+ instance_role : ParsedValue
78+ instance_type : ParsedValue
79+ mem : ParsedValue
80+ root_disk : ParsedValue
81+ root_dist_source : ParsedValue
82+ spaces : List [ParsedValue ]
83+ tags : List [ParsedValue ]
84+ virt_type : ParsedValue
85+ zones : List [ParsedValue ]
86+
87+
88+ def parse (constraints : Union [str , ConstraintsDict ]) -> Optional [ConstraintsDict ]:
6489 """
6590 Constraints must be expressed as a string containing only spaces
6691 and key value pairs joined by an '='.
@@ -69,23 +94,26 @@ def parse(constraints):
6994 if not constraints :
7095 return None
7196
72- if type (constraints ) is dict :
97+ if isinstance (constraints , dict ) :
7398 # Fowards compatibilty: already parsed
7499 return constraints
75100
76- normalized_constraints = {}
101+ normalized_constraints : ConstraintsDict = {}
77102 for s in constraints .split (" " ):
78103 if "=" not in s :
79- raise Exception ("malformed constraint %s" % s )
104+ raise ValueError ("malformed constraint %s" % s )
80105
81106 k , v = s .split ("=" )
82- normalized_constraints [normalize_key (k )] = normalize_list_value (v ) if \
83- k in LIST_KEYS else normalize_value (v )
107+ normalized_constraints [normalize_key (k )] = (
108+ normalize_list_value (v )
109+ if k in LIST_KEYS
110+ else normalize_value (v )
111+ )
84112
85113 return normalized_constraints
86114
87115
88- def normalize_key (orig_key ) :
116+ def normalize_key (orig_key : str ) -> str :
89117 key = orig_key .strip ()
90118
91119 key = key .replace ("-" , "_" ) # Our _client lib wants "_" in place of "-"
@@ -95,11 +123,11 @@ def normalize_key(orig_key):
95123 key = SNAKE2 .sub (r'\1_\2' , key ).lower ()
96124
97125 if key not in SUPPORTED_KEYS :
98- raise Exception ("unknown constraint in %s" % orig_key )
126+ raise ValueError ("unknown constraint in %s" % orig_key )
99127 return key
100128
101129
102- def normalize_value (value ) :
130+ def normalize_value (value : str ) -> Union [ int , bool , str ] :
103131 value = value .strip ()
104132
105133 if MEM .match (value ):
@@ -117,22 +145,43 @@ def normalize_value(value):
117145 return value
118146
119147
120- def normalize_list_value (value ) :
148+ def normalize_list_value (value : str ) -> List [ ParsedValue ] :
121149 values = value .strip ().split (',' )
122150 return [normalize_value (value ) for value in values ]
123151
124152
125153STORAGE = re .compile (
126- '(?:(?:^|(?<=,))(?:|(?P<pool>[a-zA-Z]+[-?a-zA-Z0-9]*)|(?P<count>-?[0-9]+)|(?:(?P<size>-?[0-9]+(?:\\ .[0-9]+)?)(?P<size_exp>[MGTPEZY])(?:i?B)?))(?:$|,))' )
127-
128-
129- def parse_storage_constraint (constraint ):
130- storage = {'count' : 1 }
154+ # original regex:
155+ # '(?:(?:^|(?<=,))(?:|(?P<pool>[a-zA-Z]+[-?a-zA-Z0-9]*)|(?P<count>-?[0-9]+)|(?:(?P<size>-?[0-9]+(?:\\.[0-9]+)?)(?P<size_exp>[MGTPEZY])(?:i?B)?))(?:$|,))'
156+ # with formatting and explanation -- note that this regex is used with re.finditer:
157+ '(?:'
158+ '(?:^|(?<=,))' # start of string or previous match ends with ','
159+ '(?:' # match one of the following:
160+ '|(?P<pool>[a-zA-Z]+[-?a-zA-Z0-9]*)' # * pool: a sequence starting with a letter, ending with a letter or number,
161+ # ------- and including letters, numbers and hyphens (no more than one in a row)
162+ '|(?P<count>-?[0-9]+)' # * count: an optional minus sign followed by one or more digits
163+ '|(?:' # * size (number) and size_exp (units):
164+ '(?P<size>-?[0-9]+(?:\\ .[0-9]+)?)' # -- * an optional minus sign followed by one or more digits, optionally with decimal point and more digits
165+ '(?P<size_exp>[MGTPEZY])(?:i?B)?)' # -- * one of MGTPEZY, optionally followed by iB or B, for example 1M or 2.0MB or -3.3MiB
166+ ')'
167+ '(?:$|,)' # end of string or ','
168+ ')'
169+ )
170+
171+
172+ class StorageConstraintDict (TypedDict ):
173+ count : Required [int ] # >= 1
174+ pool : NotRequired [str ]
175+ size : NotRequired [int ]
176+
177+
178+ def parse_storage_constraint (constraint : str ) -> StorageConstraintDict :
179+ storage : StorageConstraintDict = {'count' : 1 }
131180 for m in STORAGE .finditer (constraint ):
132181 pool = m .group ('pool' )
133182 if pool :
134183 if 'pool' in storage :
135- raise Exception ("pool already specified" )
184+ raise ValueError ("pool already specified" )
136185 storage ['pool' ] = pool
137186 count = m .group ('count' )
138187 if count :
@@ -145,15 +194,35 @@ def parse_storage_constraint(constraint):
145194
146195
147196DEVICE = re .compile (
148- '^(?P<count>[0-9]+)?(?:^|,)(?P<type>[^,]+)(?:$|,(?!$))(?P<attrs>(?:[^=]+=[^;]+)+)*$' )
197+ # original regex:
198+ # '^(?P<count>[0-9]+)?(?:^|,)(?P<type>[^,]+)(?:$|,(?!$))(?P<attrs>(?:[^=]+=[^;]+)+)*$'
199+ # with formatting and explanation -- note this regex is used with re.match:
200+ '^' # start of string
201+ '(?P<count>[0-9]+)?' # count is 1+ digits, and is optional
202+ '(?:^|,)' # match start of string or a comma
203+ # -- so type can be at the start or comma separated from count
204+ '(?P<type>[^,]+)' # type is 1+ anything not a comma (including digits), and is required
205+ '(?:$|,(?!$))' # match end of string | or a non-trailing comma
206+ # -- so type can be at the end or followed by attrs
207+ '(?P<attrs>(?:[^=]+=[^;]+)+)*' # attrs is any number of semicolon separated key=value items
208+ # -- value can have spare '=' inside, possible not intended
209+ # -- attrs will be matched with ATTR.finditer afterwards in parse_device_constraint
210+ '$' # end of string
211+ )
149212ATTR = re .compile (';?(?P<key>[^=]+)=(?P<value>[^;]+)' )
150213
151214
152- def parse_device_constraint (constraint ):
215+ class DeviceConstraintDict (TypedDict ):
216+ count : Required [int ]
217+ type : Required [str ]
218+ attributes : NotRequired [Dict [str , str ]]
219+
220+
221+ def parse_device_constraint (constraint : str ) -> DeviceConstraintDict :
153222 m = DEVICE .match (constraint )
154223 if m is None :
155- raise Exception ("device constraint does not match" )
156- device = {}
224+ raise ValueError ("device constraint does not match" )
225+ device : DeviceConstraintDict = {}
157226 count = m .group ('count' )
158227 if count :
159228 count = int (count )
0 commit comments