Skip to content

Commit 899d66d

Browse files
author
Shiyue Cheng
committed
updated w/ releases 1.0.0b10 & 0.110.4
1 parent a426e98 commit 899d66d

30 files changed

Lines changed: 1835 additions & 90 deletions

generator/async_function_template.jinja2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""
33
**{{ description }}**
44
{{ doc_url }}
5+
56
{% for d in descriptions %}
67
- {{ d }}
78
{% endfor %}

generator/function_template.jinja2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""
33
**{{ description }}**
44
{{ doc_url }}
5+
56
{% for d in descriptions %}
67
- {{ d }}
78
{% endfor %}

generator/generate_library.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ def parse_params(operation, parameters, param_filters=[]):
7474
params[k]['description'] = keys[k]['description']
7575
if 'enum' in keys[k]:
7676
params[k]['enum'] = keys[k]['enum']
77+
if 'example' in p['schema'] and k in p['schema']['example']:
78+
params[k]['example'] = p['schema']['example'][k]
7779
elif 'required' in p and p['required']:
7880
params[name] = {'required': True}
7981
params[name]['in'] = p['in']

generator/generate_snippets.py

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import json
2+
import re
3+
import shutil
4+
import sys
5+
6+
import requests
7+
from jinja2 import Template
8+
9+
10+
CALL_TEMPLATE = Template('''import meraki
11+
12+
# Defining your API key as a variable in source code is not recommended
13+
API_KEY = '6bec40cf957de430a6f1f2baa056b99a4fac9ea0'
14+
# Instead, use an environment variable as shown under the Usage section
15+
# @ https://github.com/meraki/dashboard-api-python/
16+
17+
dashboard = meraki.DashboardAPI(API_KEY)
18+
{{ parameter_assignments }}
19+
response = dashboard.{{ section }}.{{ operation}}({{ parameters }})
20+
21+
print(response)
22+
''')
23+
24+
REVERSE_PAGINATION = ['getNetworkEvents', 'getOrganizationConfigurationChanges']
25+
26+
27+
# Helper function to convert camel case parameter name to snake case
28+
def snakify(param):
29+
ret = ''
30+
for s in param:
31+
if s.islower():
32+
ret += s
33+
elif s == '_':
34+
ret += '_'
35+
else:
36+
ret += '_' + s.lower()
37+
return ret
38+
39+
40+
# Helper function to return pagination parameters depending on endpoint
41+
def generate_pagination_parameters(operation):
42+
ret = {
43+
'total_pages': {
44+
'type': 'integer or string',
45+
'description': 'total number of pages to retrieve, -1 or "all" for all pages',
46+
},
47+
'direction': {
48+
'type': 'string',
49+
'description': 'direction to paginate, either "next" or "prev" (default) page' if operation in
50+
REVERSE_PAGINATION else 'direction to paginate, either "next" (default) or "prev" page',
51+
}
52+
}
53+
return ret
54+
55+
56+
# Helper function to return parameters within OAS spec, optionally based on list of input filters
57+
def parse_params(operation, parameters, param_filters=[]):
58+
if parameters is None:
59+
return {}
60+
61+
# Create dict with information on endpoint's parameters
62+
params = {}
63+
for p in parameters:
64+
name = p['name']
65+
if 'schema' in p:
66+
keys = p['schema']['properties']
67+
for k in keys:
68+
if 'required' in p['schema'] and k in p['schema']['required']:
69+
params[k] = {'required': True}
70+
else:
71+
params[k] = {'required': False}
72+
params[k]['in'] = p['in']
73+
params[k]['type'] = keys[k]['type']
74+
params[k]['description'] = keys[k]['description']
75+
if 'enum' in keys[k]:
76+
params[k]['enum'] = keys[k]['enum']
77+
if 'example' in p['schema'] and k in p['schema']['example']:
78+
params[k]['example'] = p['schema']['example'][k]
79+
elif 'required' in p and p['required']:
80+
params[name] = {'required': True}
81+
params[name]['in'] = p['in']
82+
params[name]['type'] = p['type']
83+
if 'description' in p:
84+
params[name]['description'] = p['description']
85+
else:
86+
params[name]['description'] = '(required)'
87+
if 'enum' in p:
88+
params[name]['enum'] = p['enum']
89+
else:
90+
params[name] = {'required': False}
91+
params[name]['in'] = p['in']
92+
params[name]['type'] = p['type']
93+
params[name]['description'] = p['description']
94+
if 'enum' in p:
95+
params[name]['enum'] = p['enum']
96+
97+
# Add custom library parameters to handle pagination
98+
if 'perPage' in params:
99+
params.update(generate_pagination_parameters(operation))
100+
101+
# Return parameters based on matching input filters
102+
if not param_filters:
103+
return params
104+
else:
105+
ret = {}
106+
if 'required' in param_filters:
107+
ret.update({k: v for k, v in params.items() if 'required' in v and v['required']})
108+
if 'pagination' in param_filters:
109+
ret.update(generate_pagination_parameters(operation) if 'perPage' in params else {})
110+
if 'optional' in param_filters:
111+
ret.update({k: v for k, v in params.items() if 'required' in v and not v['required']})
112+
if 'path' in param_filters:
113+
ret.update({k: v for k, v in params.items() if 'in' in v and v['in'] == 'path'})
114+
if 'query' in param_filters:
115+
ret.update({k: v for k, v in params.items() if 'in' in v and v['in'] == 'query'})
116+
if 'body' in param_filters:
117+
ret.update({k: v for k, v in params.items() if 'in' in v and v['in'] == 'body'})
118+
if 'array' in param_filters:
119+
ret.update({k: v for k, v in params.items() if 'in' in v and v['type'] == 'array'})
120+
if 'enum' in param_filters:
121+
ret.update({k: v for k, v in params.items() if 'enum' in v})
122+
return ret
123+
124+
125+
# Generate text for parameter assignments
126+
def process_assignments(parameters):
127+
text = '\n'
128+
129+
for k, v in parameters.items():
130+
param_name = snakify(k)
131+
if param_name == 'id':
132+
param_name = 'id_'
133+
134+
if v == 'list':
135+
text += f'{param_name} = []\n'
136+
elif v == 'float':
137+
text += f'{param_name} = 0.0\n'
138+
elif v == 'int':
139+
text += f'{param_name} = 0\n'
140+
elif v == 'bool':
141+
text += f'{param_name} = False\n'
142+
elif v == 'dict':
143+
text += f'{param_name} = {{}}\n'
144+
elif v == 'str':
145+
text += f'{param_name} = \'\'\n'
146+
else:
147+
if type(v) == str:
148+
value = f'\'{v}\''
149+
else:
150+
value = v
151+
text += f'{param_name} = {value}\n'
152+
153+
return text
154+
155+
156+
def main():
157+
# Get latest OpenAPI specification
158+
spec = requests.get('https://api.meraki.com/api/v1/openapiSpec').json()
159+
160+
# Only care about the first 10 tags, which are the 10 scopes for organizations, networks, devices, & 7 products
161+
# scopes = ['organizations', 'networks', 'devices',
162+
# 'appliance', 'camera', 'cellularGateway', 'insight', 'sm', 'switch', 'wireless']
163+
tags = spec['tags']
164+
paths = spec['paths']
165+
scopes = {tag['name']: {} for tag in tags[:10]}
166+
167+
# Organize data
168+
operations = []
169+
for path, methods in paths.items():
170+
for method in methods:
171+
endpoint = paths[path][method]
172+
tags = endpoint['tags']
173+
operation = endpoint['operationId']
174+
operations.append(operation)
175+
scope = tags[0]
176+
if path not in scopes[scope]:
177+
scopes[scope][path] = {method: endpoint}
178+
else:
179+
scopes[scope][path][method] = endpoint
180+
181+
# Generate API libraries
182+
for scope in scopes:
183+
print(f'...generating {scope}')
184+
section = scopes[scope]
185+
186+
for path, methods in section.items():
187+
for method, endpoint in methods.items():
188+
# Get metadata
189+
tags = endpoint['tags']
190+
operation = endpoint['operationId']
191+
description = endpoint['summary']
192+
parameters = endpoint['parameters'] if 'parameters' in endpoint else None
193+
responses = endpoint['responses'] # not actually used here for library generation
194+
195+
required = {}
196+
optional = {}
197+
198+
if parameters:
199+
if 'perPage' in parse_params(operation, parameters):
200+
pagination = True
201+
else:
202+
pagination = False
203+
204+
for p, values in parse_params(operation, parameters, 'required').items():
205+
if 'example' in values:
206+
required[p] = values['example']
207+
elif p == 'organizationId':
208+
required[p] = '549236'
209+
elif p == 'networkId':
210+
required[p] = 'L_646829496481104079'
211+
elif p == 'serial':
212+
required[p] = 'Q2QN-9J8L-SLPD'
213+
elif values['type'] == 'array':
214+
required[p] = 'list'
215+
elif values['type'] == 'number':
216+
required[p] = 'float'
217+
elif values['type'] == 'integer':
218+
required[p] = 'int'
219+
elif values['type'] == 'boolean':
220+
required[p] = 'bool'
221+
elif values['type'] == 'object':
222+
required[p] = 'dict'
223+
elif values['type'] == 'string':
224+
required[p] = 'str'
225+
else:
226+
sys.exit(p, values)
227+
228+
if pagination:
229+
if operation not in REVERSE_PAGINATION:
230+
optional['total_pages'] = 'all'
231+
else:
232+
optional['total_pages'] = 3
233+
234+
for p, values in parse_params(operation, parameters, 'optional').items():
235+
if 'example' in values:
236+
optional[p] = values['example']
237+
238+
if operation == 'createNetworkGroupPolicy':
239+
print(required)
240+
print(optional)
241+
242+
with open(f'code_snippets/{operation}.py', 'w') as fp:
243+
if required.items():
244+
parameters_text = '\n '
245+
for k, v in required.items():
246+
param_name = snakify(k)
247+
if param_name == 'id':
248+
param_name = 'id_'
249+
parameters_text += f'{param_name}, '
250+
for k, v in optional.items():
251+
if k == 'total_pages' and v == 'all':
252+
parameters_text += f'total_pages=\'all\''
253+
elif k == 'total_pages' and v == 1:
254+
parameters_text += f'total_pages=1'
255+
elif type(v) == str:
256+
parameters_text += f'\n {k}=\'{v}\', '
257+
else:
258+
parameters_text += f'\n {k}={v}, '
259+
if parameters_text[-2:] == ', ':
260+
parameters_text = parameters_text[:-2]
261+
parameters_text += '\n'
262+
else:
263+
parameters_text = ''
264+
265+
266+
fp.write(
267+
CALL_TEMPLATE.render(
268+
parameter_assignments=process_assignments(required),
269+
section=scope,
270+
operation=operation,
271+
parameters=parameters_text,
272+
)
273+
)
274+
275+
276+
if __name__ == '__main__':
277+
main()

meraki/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
)
8989

9090

91-
__version__ = '0.110.3'
91+
__version__ = '0.110.4'
9292

9393

9494
class DashboardAPI(object):

meraki/aio/rest_session.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ def __init__(
7979
super().__init__()
8080

8181
# Initialize attributes and properties
82+
self._version = __version__
8283
self._api_key = str(api_key)
8384
self._base_url = str(base_url)
8485
self._single_request_timeout = single_request_timeout
@@ -107,7 +108,7 @@ def __init__(
107108
self._headers = {
108109
'X-Cisco-Meraki-API-Key': self._api_key,
109110
"Content-Type": "application/json",
110-
"User-Agent": f"python-meraki/aio-{__version__} " + user_agent_extended(self._be_geo_id, self._caller),
111+
"User-Agent": f"python-meraki/aio-{self._version} " + user_agent_extended(self._be_geo_id, self._caller),
111112
}
112113
if self._certificate_path:
113114
self._sslcontext = ssl.create_default_context()
@@ -149,7 +150,7 @@ async def _request(self, metadata, method, url, **kwargs):
149150
kwargs.setdefault("timeout", self._single_request_timeout)
150151

151152
# Ensure proper base URL
152-
if "meraki.com" in url:
153+
if "meraki.com" in url or "meraki.cn" in url:
153154
abs_url = url
154155
else:
155156
abs_url = self._base_url + url
@@ -203,6 +204,8 @@ async def _request(self, metadata, method, url, **kwargs):
203204
elif 300 <= status < 400:
204205
abs_url = response.headers["Location"]
205206
substring = "meraki.com/api/v"
207+
if substring not in abs_url:
208+
substring = "meraki.cn/api/v"
206209
self._base_url = abs_url[
207210
: abs_url.find(substring) + len(substring) + 1
208211
]

meraki/rest_session.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def __init__(
7777
super(RestSession, self).__init__()
7878

7979
# Initialize attributes and properties
80+
self._version = __version__
8081
self._api_key = str(api_key)
8182
self._base_url = str(base_url)
8283
self._single_request_timeout = single_request_timeout
@@ -107,7 +108,7 @@ def __init__(
107108
self._req_session.headers = {
108109
'X-Cisco-Meraki-API-Key': self._api_key,
109110
'Content-Type': 'application/json',
110-
'User-Agent': f'python-meraki/{__version__} ' + user_agent_extended(self._be_geo_id, self._caller),
111+
'User-Agent': f'python-meraki/{self._version} ' + user_agent_extended(self._be_geo_id, self._caller),
111112
}
112113

113114
# Log API calls
@@ -130,7 +131,7 @@ def request(self, metadata, method, url, **kwargs):
130131
kwargs.setdefault('timeout', self._single_request_timeout)
131132

132133
# Ensure proper base URL
133-
if 'meraki.com' in url:
134+
if 'meraki.com' in url or 'meraki.cn' in url:
134135
abs_url = url
135136
else:
136137
abs_url = self._base_url + url
@@ -169,6 +170,8 @@ def request(self, metadata, method, url, **kwargs):
169170
if str(status)[0] == '3':
170171
abs_url = response.headers['Location']
171172
substring = 'meraki.com/api/v'
173+
if substring not in abs_url:
174+
substring = 'meraki.cn/api/v'
172175
self._base_url = abs_url[:abs_url.find(substring) + len(substring) + 1]
173176

174177
# 2XX success

meraki_v1/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
SUPPRESS_LOGGING, SIMULATE_API_CALLS, BE_GEO_ID, MERAKI_PYTHON_SDK_CALLER
2121
)
2222

23-
__version__ = '1.0.0b9'
23+
__version__ = '1.0.0b10'
2424

2525
class DashboardAPI(object):
2626
"""

0 commit comments

Comments
 (0)