198 |
199 |
407 |
408 |
409 |
--------------------------------------------------------------------------------
/CoderCardsClient/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "scripts": {
4 | "start": "live-server",
5 | "test": "echo \"Error: no test specified\" && exit 1"
6 | },
7 | "devDependencies": {
8 | "live-server":"1.2.0"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Donna Malayeri
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | services: functions
3 | platforms: dotnet
4 | author: lindydonna
5 | ---
6 |
7 | # CoderCards - trading card generator
8 |
9 | CoderCards is a geek trading card generator. It uses Microsoft Cognitive Services to detect the predominant emotion in a face, which is used to choose a card back.
10 |
11 | The sample demonstrates the following features
12 | - C# attributes and Visual Studio 2017 tooling
13 | - Functions backing a SPA, hosted in Azure Storage
14 | - Azure Functions proxies to customize the site index.html
15 |
16 | There's also a C# script version of this sample: [CoderCards](https://github.com/lindydonna/codercards).
17 |
18 | [](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Flindydonna%2FCoderCardsV2%2Fmaster%2FAzureDeploy%2Fazuredeploy.json)
19 |
20 | ## Prerequisites
21 |
22 | * To build the functions project, use [Visual Studio 15.3 Preview](https://www.visualstudio.com/vs/preview/) and the [Azure Functions Tooling VSIX](https://marketplace.visualstudio.com/items?itemName=AndrewBHall-MSFT.AzureFunctionToolsforVisualStudio2017).
23 |
24 | * To run the setup script, install the [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli).
25 |
26 | ## About the sample
27 |
28 | * There are two functions defined in this project:
29 | * **RequestImageProcessing**. HTTP trigger that writes a queue message. The request payload must be in the following form:
30 |
31 | ```json
32 | {
33 | "PersonName": "Scott Guthrie",
34 | "Title": "Red Polo Connoisseur",
35 | "BlobName": "Scott Guthrie-Red Polo Connoisseur.jpg"
36 | }
37 | ```
38 |
39 | * **GenerateCard**. Queue trigger that binds to the blob specified in the BlobName property of the queue payload. Based on the predominant emotion of the input image, it generates a card using one of 4 card templates.
40 |
41 | * The card is written to the output blob container specified by the app setting `output-container`.
42 |
43 | Here's a visualization of the bindings, using the [Azure Functions Bindings Visualizer](https://functions-visualizer.azurewebsites.net):
44 |
45 | 
46 |
47 | ## Setup
48 |
49 | ### Setup script
50 |
51 | Use the Python setup script [setup.py](setup.py). This uses the Azure CLI 2.0 to automate the storage account setup. Run the following commands:
52 |
53 | ```
54 | az login
55 | python setup.py storage-account resource-group true
56 | ```
57 |
58 | This will modify the file [local.settings.json](CoderCards/local.settings.json). The last argument controls whether to create containers prefixed with "local".
59 |
60 | Alternatively, you can run the script from the Azure Cloud Shell in the Azure Portal. Just run `python` and paste the script. The script prints out settings values that you can use to manually modify `local.settings.json`.
61 |
62 | ### Required App Settings
63 |
64 | | Key | Description |
65 | |----- | ------|
66 | | AzureWebJobsStorage | Storage account connection string |
67 | | EmotionAPIKey | Key for [Cognitive Services Emotion API](https://www.microsoft.com/cognitive-services/en-us/emotion-api) |
68 | | input-queue | Name of Storage queue for to trigger card generation. Use a value like "local-queue" locally and "input-queue" on Azure
69 | | input-container | Name of Storage container for input images. Use a value like "local-card-input" locally and "card-input" on Azure |
70 | | output-container | Name of Storage container for output images. Use a value like "local-card-output" locally and "card-output" on Azure |
71 | | SITEURL | Set to `http://localhost:7071` locally. Not required on Azure. |
72 | | STORAGE_URL | URL of storage account, in the form `https://accountname.blob.core.windows.net/` |
73 | | CONTAINER_SAS | SAS token for uploading to input-container. Include the "?" prefix. |
74 |
75 | If you want to set these values in Azure, you can set them in *local.settings.json* and use the Azure Functions Core Tools to publish to Azure.
76 |
77 | ```
78 | python setup.py storage-account resource-group false
79 | func azure functionapp publish function-app-name --publish-app-settings
80 | ```
81 |
82 | ## Local debugging in Visual Studio
83 |
84 | - If you're using Visual Studio 2017 Update 3 and the Azure Functions Tools VSIX, open the project [CoderCards.csproj](CoderCards/CoderCards.csproj). F5 will automatically launch the Azure Functions Core tools.
85 |
86 | - The project has a custom launchSettings.json that passes these arguments to the Functions Core Tools: `host start --cors * --pause-on-error`.
87 |
88 | ## Running the demo
89 |
90 | ### Running using the provided SPA webpage
91 |
92 | Make sure the functions host is running locally via Visual Studio or the Azure Functions Core Tools.
93 |
94 | In a command prompt, go to the `CoderCardsClient` directory.
95 |
96 | - Run `npm install`
97 | - Run `npm start`. This will launch a webpage at `http://127.0.0.1:8080/`. Navigate instead to `http://localhost:8080`.
98 |
99 | ### Running manually
100 | 1. Choose images that are **square** and upload to the `card-input` container. (Images that aren't square will be stretched.)
101 | 2. Send an HTTP request using Postman or CURL, specifying the path of the blob you just uploaded:
102 |
103 | ```json
104 | {
105 | "PersonName": "My Name",
106 | "Title": "My Title",
107 | "BlobName": "BlobFilename.jpg"
108 | }
109 | ```
110 |
111 | ## Notes
112 |
113 | * The demo uses System.Drawing, which is NOT recommended for production apps. To learn more, see [5 Reasons You Should Stop Using System\.Drawing from ASP\.NET](http://photosauce.net/blog/post/5-reasons-you-should-stop-using-systemdrawing-from-aspnet).
114 |
115 | * Happy faces get a multiplier of 4, angry gets a multiplier of 2. I encourage you to tweak for maximum comedic effect!
116 |
117 | ## Talking points about Azure Functions
118 |
119 | * Creating an HTTP trigger that writes a queue message is just one line of code!
120 |
121 | * Using a queue message to trigger blob processing is preferable to a blob trigger, as it is easier to ensure transactional processing. Blob triggers can be [delayed for up to 10 minutes on the Consumption plan](https://docs.microsoft.com/en-us/azure/azure-functions/functions-scale#how-the-consumption-plan-works)
122 |
123 | * By binding to a POCO, you can use the payload of a trigger to configure an input binding. In this example, we binding to the `BlobName` property in the queue message.
124 |
125 | * The input binding is just a byte array, which makes it easy to manipulate with memory streams (no need to create new ones). Other binding types for C# are Stream, CloudBlockBlob, etc, which is very flexible. The output binding is just a stream that you just write to.
126 |
127 | ## Next steps
128 |
129 | For more information about the Azure Functions Visual Studio tooling, see the following:
130 |
131 | - [Visual Studio 2017 Tools for Azure Functions](https://docs.microsoft.com/en-us/azure/azure-functions/functions-develop-vs)
132 | - [Using \.NET class libraries with Azure Functions](https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-class-library)
133 | - [Code and test Azure functions locally](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local)
134 | - Video: [Azure Functions Visual Studio Tooling](https://www.youtube.com/watch?v=BN2sIRrOt8A)
135 | - Video: [Cloud Cover: Azure Functions Local Debugging and More with Donna Malayeri](https://channel9.msdn.com/Shows/Cloud+Cover/Episode-231-Azure-Functions-Local-Debugging-and-More-with-Donna-Malayeri)
136 |
--------------------------------------------------------------------------------
/function-bindings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/functions-dotnet-codercards/HEAD/function-bindings.png
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | settingsFilename = "CoderCards/local.settings.json"
4 | containerContent = "content"
5 | indexHtmlFile = "CoderCardsClient/index.html"
6 |
7 | def runCommand(str):
8 | return subprocess.run(str, stdout=subprocess.PIPE, shell=True).stdout.decode('utf-8').rstrip()
9 |
10 | def str2bool(v):
11 | return v.lower() in ("yes", "true", "t", "1")
12 |
13 | import fileinput, re, sys, getopt, subprocess
14 |
15 | if (len(sys.argv) < 3):
16 | print("Usage: {} ".format(sys.argv[0]))
17 | sys.exit();
18 |
19 | storageName = sys.argv[1] # storage account name
20 | resourceGroup = sys.argv[2] # storage resource group
21 | isLocal = str2bool(sys.argv[3]) # local or prod containers
22 |
23 | containerInput = "input-local" if isLocal else "card-input"
24 | containerOutput = "output-local" if isLocal else "card-output"
25 | queueName = "local-queue" if isLocal else "input-queue"
26 |
27 | # Retrieve the Storage Account connection string
28 | connstr = runCommand('az storage account show-connection-string --name {} --resource-group {} --query connectionString --output tsv'.format(storageName, resourceGroup))
29 |
30 | # get account URL
31 | accountUrl = \
32 | runCommand('az storage account show --name {} -g {} --output tsv --query "{{primaryEndpoints:primaryEndpoints}}.primaryEndpoints.blob"'.format(storageName, resourceGroup))
33 |
34 | # create containers
35 | runCommand('az storage container create --connection-string "{}" --name {}'.format(connstr, containerInput))
36 | runCommand('az storage container create --connection-string "{}" --name {}'.format(connstr, containerOutput))
37 | runCommand('az storage container create --connection-string "{}" --name {}'.format(connstr, containerContent))
38 |
39 | # get SAS token for input container
40 | sasToken = runCommand('az storage container generate-sas --connection-string "{}" --name {} --permissions lrw --expiry 2018-01-01 -o tsv'.format(connstr, containerInput))
41 |
42 | # set permissions on output and content containers
43 | runCommand('az storage container set-permission --connection-string "{}" --public-access blob -n {}'.format(connstr, containerOutput))
44 | runCommand('az storage container set-permission --connection-string "{}" --public-access blob -n {}'.format(connstr, containerContent))
45 |
46 | # upload index.html to storage
47 | runCommand('az storage blob upload --connection-string "{}" --container-name {} -f {} -n index.html --content-type "text/html"'.format(connstr, containerContent, indexHtmlFile))
48 |
49 | # set CORS on blobs
50 | runCommand('az storage cors add --connection-string "{}" --origins "*" --methods GET PUT OPTIONS --allowed-headers "*" --exposed-headers "*" --max-age 200 --services b'.format(connstr))
51 |
52 |
53 | # new settings values
54 | settingAzureWebJobsStorage = '"AzureWebJobsStorage": "{}"'.format(connstr)
55 | settingStorageUrl = '"STORAGE_URL": "{}"'.format(accountUrl)
56 | settingContainerSas = '"CONTAINER_SAS": "?{}"'.format(sasToken)
57 |
58 | # write out new settings values
59 | print(settingAzureWebJobsStorage)
60 | print(settingStorageUrl)
61 | print(settingContainerSas)
62 |
63 | # write changes to file
64 | with open(settingsFilename, 'r') as file:
65 | filedata = file.read()
66 |
67 | filedata = filedata.replace('"AzureWebJobsStorage": ""', settingAzureWebJobsStorage) \
68 | .replace('"STORAGE_URL": ""', settingStorageUrl) \
69 | .replace('"CONTAINER_SAS": ""', settingContainerSas)
70 |
71 | filedata = re.sub(r'"input-container": .*,', '"input-container": "{}",'.format(containerInput), filedata)
72 | filedata = re.sub(r'"output-container": .*,', '"output-container": "{}",'.format(containerOutput), filedata)
73 | filedata = re.sub(r'"input-queue": .*,', '"input-queue": "{}",'.format(queueName), filedata)
74 |
75 | with open(settingsFilename, 'w') as file:
76 | file.write(filedata)
77 |
--------------------------------------------------------------------------------