Skip to content

Commit 8d3c32a

Browse files
Update Azure Maps sample documentation and enhance async request handling in ApimRequests
1 parent 816abf8 commit 8d3c32a

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
@@ -146,6 +146,7 @@ For detailed troubleshooting of setup issues, see [Import Troubleshooting Guide]
146146
| [General](./samples/general/README.md) | Basic demo of APIM sample setup and policy usage. | All infrastructures |
147147
| [Load Balancing](./samples/load-balancing/README.md) | Priority and weighted load balancing across backends. | apim-aca, afd-apim (with ACA) |
148148
| [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 |
149+
| [Azure Maps](./samples/azure-maps/README.md) | Proxying calls to Azure Maps with APIM policies. | All infrastructures |
149150

150151
### ▶️ Running a Sample
151152

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
@@ -212,6 +212,43 @@ def _print_response_code(self, response) -> None:
212212

213213
utils.print_val("Response status", status_code_str)
214214

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

216253
# ------------------------------
217254
# PUBLIC METHODS
@@ -264,4 +301,93 @@ def multiGet(self, path: str, runs: int, headers = None, data = None, msg: str |
264301
List of response dicts for each run.
265302
"""
266303

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

0 commit comments

Comments
 (0)