Skip to content

Commit cc146c4

Browse files
Update Azure Maps sample documentation and enhance async request handling in ApimRequests
1 parent 3879831 commit cc146c4

File tree

5 files changed

+149
-27
lines changed

5 files changed

+149
-27
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ The first time you run a Jupyter notebook, you'll be asked to install the Jupyte
6767
| [General](./samples/general/README.md) | Basic demo of APIM sample setup and policy usage. | All infrastructures |
6868
| [Load Balancing](./samples/load-balancing/README.md) | Priority and weighted load balancing across backends. | apim-aca, afd-apim (with ACA) |
6969
| [Secure Blob Access](./samples/secure-blob-access/README.md) | Secure blob access via the [valet key pattern](https://learn.microsoft.com/azure/architecture/patterns/valet-key). | All infrastructures |
70+
| [Azure Maps](./samples/azure-maps/README.md) | Proxying calls to Azure Maps with APIM policies. | All infrastructures |
7071

7172
### ▶️ Running a Sample
7273

samples/azure-maps/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ This sample demonstrates how to use APIM to proxy requests to the Azure Maps ser
2222
This lab sets up:
2323

2424
- An Azure Maps resource in Azure
25-
- APIM managed identity with Storage Blob Data Reader permissions
25+
- APIM managed identity with the following roles:
26+
- **Azure Maps Search and Render Data Reader:** Grants the ability to call the apis and render the maps
27+
- **Azure Maps Contributor:** Grants the ability to create the SAS Token from the APIM policy
28+
- A User Assigned Managed Identity (UAMI) that is used as the principal id to emulate when creating the SAS Token for Azure Maps. It has the following roles asigned:
29+
- **Azure Maps Search and Render Data Reader:** Grants the ability to call the apis and render the maps
2630
- An API that demonstrates proxying requests to Azure Maps specific to APIs (geocode, search, etc.)
2731
- Also in that api there will be an operation that demonstrates a generic path to Azure Maps
2832

samples/azure-maps/create.ipynb

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
"\n",
99
"Configures everything that's needed for deployment. \n",
1010
"\n",
11-
"[ADD ANY SPECIAL INSTRUCTIONS]\n",
12-
"\n",
1311
"**Modify entries under _1) User-defined parameters_ and _3) Define the APIs and their operations and policies_**."
1412
]
1513
},
@@ -49,14 +47,11 @@
4947
"mapApi_v2_geocode_get = GET_APIOperation2('get-geocode','Get Geocode','/geocode','Get geocode endpoint',map_geocode_v2_aad_get_xml)\n",
5048
"api1 = API('map-api', 'Map API', '/map', 'This is the proxy for Azure Maps', operations=[mapApi_v2_default_get, mapApi_v1_async_post,mapApi_v2_geocode_get], tags = tags, serviceUrl=azure_maps_url)\n",
5149
"\n",
52-
"# API n\n",
53-
"# ...\n",
54-
"\n",
5550
"# APIs Array\n",
5651
"# apis: List[API] = [api1, apin]\n",
5752
"apis: List[API] = [api1]\n",
5853
"\n",
59-
"# 4) Set up the named values\n",
54+
"# 4) Set up the named values, for this specific sample, we are using some of the named values in the API policies defined above that can't be known at this point in the process. For those named values, we are setting them in the main.bicep file.\n",
6055
"nvs: List[NamedValue] = [\n",
6156
" NamedValue('azure-maps-arm-api-version','2023-06-01')\n",
6257
"]\n",
@@ -125,35 +120,23 @@
125120
"import utils\n",
126121
"from apimrequests import ApimRequests\n",
127122
"\n",
128-
"# [ADD RELEVANT TESTS HERE]\n",
123+
"reqs = ApimRequests(apim_gateway_url)\n",
129124
"\n",
130125
"# 1) Issue a direct request to API Management\n",
131-
"# reqsApim = ApimRequests(apim_gateway_url)\n",
132-
"# reqsApim.singleGet('/request-headers', msg = 'Calling Request Headers API via API Management Gateway URL. Response codes 200 and 403 are both valid depending on the infrastructure used.')\n",
133-
"\n",
134-
"# # 2) Issue requests against Front Door.\n",
135-
"# # Check if the infrastructure architecture deployment uses Azure Front Door.\n",
136-
"# utils.print_message('Checking if the infrastructure architecture deployment uses Azure Front Door.', blank_above = True)\n",
137-
"# afd_endpoint_url = utils.get_frontdoor_url(deployment, rg_name)\n",
138-
"\n",
139-
"# if afd_endpoint_url:\n",
140-
"# reqsAfd = ApimRequests(afd_endpoint_url)\n",
141-
"# reqsAfd.singleGet('/request-headers', msg = 'Calling Request Headers API via via Azure Front Door. Expect 200.')\n",
142-
"\n",
143-
"reqs = ApimRequests(apim_gateway_url)\n",
126+
"reqs.singleGet('/', msg = 'Calling Hello World (Root) API. Expect 200.')\n",
144127
"\n",
145-
"reqs.singleGet('/', msg = 'Calling Hello World (Root) API')\n",
146-
"reqs.singleGet('/map/default/geocode?query=15127%20NE%2024th%20Street%20Redmond%20WA', msg = 'Calling Default Route API with AAD Auth')\n",
147-
"reqs.singleGet('/map/geocode?query=15127%20NE%2024th%20Street%20Redmond%20WA', msg = 'Calling Geocode v2 API with AAD Auth')\n",
148-
"reqs.singlePost('/map/geocode/batch/async', data={\n",
128+
"# 2) Issue requests to API Management with Azure Maps APIs\n",
129+
"reqs.singleGet('/map/default/geocode?query=15127%20NE%2024th%20Street%20Redmond%20WA', msg = 'Calling Default Route API with AAD Auth. Expect 200.')\n",
130+
"reqs.singleGet('/map/geocode?query=15127%20NE%2024th%20Street%20Redmond%20WA', msg = 'Calling Geocode v2 API with AAD Auth. Expect 200.')\n",
131+
"reqs.singlePostAsync('/map/geocode/batch/async', data={\n",
149132
" \"batchItems\": [\n",
150133
" {\"query\": \"?query=400 Broad St, Seattle, WA 98109&limit=3\"},\n",
151134
" {\"query\": \"?query=One, Microsoft Way, Redmond, WA 98052&limit=3\"},\n",
152135
" {\"query\": \"?query=350 5th Ave, New York, NY 10118&limit=1\"},\n",
153136
" {\"query\": \"?query=Pike Pl, Seattle, WA 98101&lat=47.610970&lon=-122.342469&radius=1000\"},\n",
154137
" {\"query\": \"?query=Champ de Mars, 5 Avenue Anatole France, 75007 Paris, France&limit=1\"}\n",
155138
" ]\n",
156-
"}, msg = 'Calling Async Geocode Batch v1 API with Key Auth')\n",
139+
"}, msg = 'Calling Async Geocode Batch v1 API with Key Auth. Expect 200', timeout=120, poll_interval=3)\n",
157140
"\n",
158141
"utils.print_ok('All done!')"
159142
]

samples/azure-maps/main.bicep

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,14 @@ module apisModule '../../shared/bicep/modules/apim/v1/api.bicep' = [for api in a
147147
appInsightsId: appInsightsId
148148
api: api
149149
}
150+
dependsOn: [
151+
mapsSubscriptionKeyNamedValue
152+
mapsClientIdNamedValue
153+
userAssignedIdentityObjectIdNamedValue
154+
subscriptionIdNamedValue
155+
resourceGroupNamedValue
156+
azureMapsResourceNamedValue
157+
]
150158
}]
151159

152160
// Grant APIM managed identity access to Azure Maps, here are the RBAC roles you might need: https://learn.microsoft.com/en-us/azure/azure-maps/azure-maps-authentication#picking-a-role-definition

shared/python/apimrequests.py

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,43 @@ def _print_response_code(self, response):
208208

209209
utils.print_val("Response status", status_code_str)
210210

211+
def _poll_async_operation(self, location_url: str, headers: dict = None, timeout: int = 60, poll_interval: int = 2) -> requests.Response | None:
212+
"""
213+
Poll an async operation until completion.
214+
215+
Args:
216+
location_url: The URL from the Location header
217+
headers: Headers to include in polling requests
218+
timeout: Maximum time to wait in seconds
219+
poll_interval: Time between polls in seconds
220+
221+
Returns:
222+
The final response when operation completes or None on error
223+
"""
224+
start_time = time.time()
225+
226+
while time.time() - start_time < timeout:
227+
try:
228+
response = requests.get(location_url, headers=headers or {})
229+
230+
utils.print_info(f"Polling operation - Status: {response.status_code}")
231+
232+
if response.status_code == 200:
233+
utils.print_ok("Async operation completed successfully!")
234+
return response
235+
elif response.status_code == 202:
236+
utils.print_info(f"Operation still in progress, waiting {poll_interval} seconds...")
237+
time.sleep(poll_interval)
238+
else:
239+
utils.print_error(f"Unexpected status code during polling: {response.status_code}")
240+
return response
241+
242+
except requests.exceptions.RequestException as e:
243+
utils.print_error(f"Error polling operation: {e}")
244+
return None
245+
246+
utils.print_error(f"Async operation timeout reached after {timeout} seconds")
247+
return None
211248

212249
# ------------------------------
213250
# PUBLIC METHODS
@@ -260,4 +297,93 @@ def multiGet(self, path: str, runs: int, headers = None, data = None, msg: str |
260297
List of response dicts for each run.
261298
"""
262299

263-
return self._multiRequest(method = HTTP_VERB.GET, path = path, runs = runs, headers = headers, data = data, msg = msg, printResponse = printResponse, sleepMs = sleepMs)
300+
return self._multiRequest(method = HTTP_VERB.GET, path = path, runs = runs, headers = headers, data = data, msg = msg, printResponse = printResponse, sleepMs = sleepMs)
301+
302+
def singlePostAsync(self, path: str, *, headers = None, data = None, msg: str | None = None, printResponse = True, timeout = 60, poll_interval = 2) -> Any:
303+
"""
304+
Make an async POST request to the Azure API Management service and poll until completion.
305+
306+
Args:
307+
path: The path to append to the base URL for the request.
308+
headers: Additional headers to include in the request.
309+
data: Data to include in the request body.
310+
msg: Optional message to display.
311+
printResponse: Whether to print the returned output.
312+
timeout: Maximum time to wait for completion in seconds.
313+
poll_interval: Time between polls in seconds.
314+
315+
Returns:
316+
str | None: The JSON response as a string, or None on error.
317+
"""
318+
319+
try:
320+
if msg:
321+
utils.print_message(msg, blank_above = True)
322+
323+
url = self.url + path
324+
utils.print_info(f"POST {url}")
325+
326+
merged_headers = self.headers.copy()
327+
328+
if headers:
329+
merged_headers.update(headers)
330+
331+
# Make the initial async request
332+
response = requests.request(HTTP_VERB.POST.value, url, headers = merged_headers, json = data)
333+
334+
utils.print_info(f"Initial response status: {response.status_code}")
335+
336+
if response.status_code == 202: # Accepted - async operation started
337+
location_header = response.headers.get('Location')
338+
if location_header:
339+
utils.print_info(f"Found Location header: {location_header}")
340+
341+
# Poll the location URL until completion
342+
final_response = self._poll_async_operation(
343+
location_header,
344+
headers=merged_headers,
345+
timeout=timeout,
346+
poll_interval=poll_interval
347+
)
348+
349+
if final_response and final_response.status_code == 200:
350+
if printResponse:
351+
self._print_response(final_response)
352+
353+
content_type = final_response.headers.get('Content-Type')
354+
responseBody = None
355+
356+
if content_type and 'application/json' in content_type:
357+
responseBody = json.dumps(final_response.json(), indent = 4)
358+
else:
359+
responseBody = final_response.text
360+
361+
return responseBody
362+
else:
363+
utils.print_error("Async operation failed or timed out")
364+
return None
365+
else:
366+
utils.print_error("No Location header found in 202 response")
367+
if printResponse:
368+
self._print_response(response)
369+
return None
370+
else:
371+
# Non-async response, handle normally
372+
if printResponse:
373+
self._print_response(response)
374+
375+
content_type = response.headers.get('Content-Type')
376+
responseBody = None
377+
378+
if content_type and 'application/json' in content_type:
379+
responseBody = json.dumps(response.json(), indent = 4)
380+
else:
381+
responseBody = response.text
382+
383+
return responseBody
384+
385+
except requests.exceptions.RequestException as e:
386+
utils.print_error(f"Error making request: {e}")
387+
return None
388+
389+

0 commit comments

Comments
 (0)