Skip to content

Commit f032367

Browse files
khkh-msGavin Aguiar
andauthored
Python V2 (#3288)
* Python V2 changes. * Defaulting V2 * updated tests * Python v2. Changes for updated templates. * Took UserInputHandler into different file. * blue print changes. * More templates updates. * Removed the old templates * Updated the init template. * Fix for the help * V2 is not be default as decided by python team. Switch back v1 as default. * Fixing minor bugs --------- Co-authored-by: Gavin Aguiar <gavin@GavinPC>
1 parent 9142b97 commit f032367

File tree

15 files changed

+1534
-36
lines changed

15 files changed

+1534
-36
lines changed

src/Azure.Functions.Cli/Actions/LocalActions/CreateFunctionAction.cs

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4+
using System.IO.Abstractions;
45
using System.Linq;
6+
using System.Runtime.CompilerServices;
57
using System.Text.RegularExpressions;
68
using System.Threading.Tasks;
79
using Azure.Functions.Cli.Common;
810
using Azure.Functions.Cli.ExtensionBundle;
11+
using Azure.Functions.Cli.Extensions;
912
using Azure.Functions.Cli.Helpers;
1013
using Azure.Functions.Cli.Interfaces;
1114
using Colors.Net;
1215
using Fclp;
16+
using ImTools;
17+
using Microsoft.Azure.AppService.Proxy.Common.Context;
1318
using Microsoft.Azure.WebJobs.Extensions.Http;
19+
using Microsoft.Azure.WebJobs.Script;
1420
using Newtonsoft.Json;
1521
using Newtonsoft.Json.Linq;
1622
using static Azure.Functions.Cli.Common.Constants;
@@ -26,8 +32,9 @@ internal class CreateFunctionAction : BaseAction
2632
private ITemplatesManager _templatesManager;
2733
private readonly ISecretsManager _secretsManager;
2834
private readonly IContextHelpManager _contextHelpManager;
29-
35+
private readonly IUserInputHandler _userInputHandler;
3036
private readonly InitAction _initAction;
37+
Lazy<IEnumerable<UserPrompt>> _userPrompts;
3138
public WorkerRuntime workerRuntime;
3239

3340
public string Language { get; set; }
@@ -39,15 +46,18 @@ internal class CreateFunctionAction : BaseAction
3946
public AuthorizationLevel? AuthorizationLevel { get; set; }
4047

4148
Lazy<IEnumerable<Template>> _templates;
42-
49+
Lazy<IEnumerable<NewTemplate>> _newTemplates;
4350

4451
public CreateFunctionAction(ITemplatesManager templatesManager, ISecretsManager secretsManager, IContextHelpManager contextHelpManager)
4552
{
4653
_templatesManager = templatesManager;
4754
_secretsManager = secretsManager;
4855
_contextHelpManager = contextHelpManager;
4956
_initAction = new InitAction(_templatesManager, _secretsManager);
57+
_userInputHandler = new UserInputHandler(_templatesManager);
5058
_templates = new Lazy<IEnumerable<Template>>(() => { return _templatesManager.Templates.Result; });
59+
_newTemplates = new Lazy<IEnumerable<NewTemplate>>(() => { return _templatesManager.NewTemplates.Result; });
60+
_userPrompts = new Lazy<IEnumerable<UserPrompt>>(() => { return _templatesManager.UserPrompts.Result; });
5161
}
5262

5363
public override ICommandLineParserResult ParseArgs(string[] args)
@@ -104,17 +114,6 @@ public async override Task RunAsync()
104114

105115
await UpdateLanguageAndRuntime();
106116

107-
// Check if the programming model is PyStein
108-
if (IsNewPythonProgrammingModel())
109-
{
110-
// TODO: Remove these messages once creating new functions in the new programming model is supported
111-
ColoredConsole.WriteLine(WarningColor("When using the new Python programming model, triggers and bindings are created as decorators within the Python file itself."));
112-
ColoredConsole.Write(AdditionalInfoColor("For information on how to create a new function with the new programming model, see "));
113-
PythonHelpers.PrintPySteinWikiLink();
114-
throw new CliException(
115-
"Function not created! 'func new' is not supported for the preview of the V2 Python programming model.");
116-
}
117-
118117
if (WorkerRuntimeLanguageHelper.IsDotnet(workerRuntime) && !Csx)
119118
{
120119
if (string.IsNullOrWhiteSpace(TemplateName))
@@ -133,6 +132,55 @@ public async override Task RunAsync()
133132
var namespaceStr = Path.GetFileName(Environment.CurrentDirectory);
134133
await DotnetHelpers.DeployDotnetFunction(TemplateName.Replace(" ", string.Empty), Utilities.SanitizeClassName(FunctionName), Utilities.SanitizeNameSpace(namespaceStr), Language.Replace("-isolated", ""), workerRuntime, AuthorizationLevel);
135134
}
135+
else if (IsNewPythonProgrammingModel())
136+
{
137+
if (string.IsNullOrEmpty(TemplateName))
138+
{
139+
SelectionMenuHelper.DisplaySelectionWizardPrompt("template");
140+
TemplateName = TemplateName ?? SelectionMenuHelper.DisplaySelectionWizard(GetTriggerNamesFromNewTemplates(Language));
141+
}
142+
143+
if (string.IsNullOrEmpty(FileName))
144+
{
145+
var userPrompt = _userPrompts.Value.First(x => string.Equals(x.Id, "app-selectedFileName", StringComparison.OrdinalIgnoreCase));
146+
while (!_userInputHandler.ValidateResponse(userPrompt, FileName))
147+
{
148+
_userInputHandler.PrintInputLabel(userPrompt, PySteinFunctionAppPy);
149+
FileName = Console.ReadLine();
150+
if (string.IsNullOrEmpty(FileName))
151+
{
152+
FileName = PySteinFunctionAppPy;
153+
}
154+
}
155+
}
156+
157+
var variables = new Dictionary<string, string>();
158+
var jobName = "appendToFile";
159+
if (FileName != PySteinFunctionAppPy)
160+
{
161+
var filePath = Path.Combine(Environment.CurrentDirectory, FileName);
162+
jobName = !FileUtility.FileExists(filePath) ? "CreateNewBlueprint" : "AppendToBlueprint";
163+
variables["$(BLUEPRINT_FILENAME)"] = FileName;
164+
FileName = FileName[..^Path.GetExtension(FileName).Length];
165+
}
166+
else
167+
{
168+
variables["$(SELECTED_FILEPATH)"] = FileName;
169+
}
170+
171+
var template = _newTemplates.Value.FirstOrDefault(t => string.Equals(t.Name, TemplateName, StringComparison.CurrentCultureIgnoreCase) && string.Equals(t.Language, Language, StringComparison.CurrentCultureIgnoreCase));
172+
var templateJob = template.Jobs.Single(x => x.Type.Equals(jobName, StringComparison.OrdinalIgnoreCase));
173+
var providedInputs = new Dictionary<string, string>() { { GetFunctionNameParamId, FunctionName } };
174+
175+
_userInputHandler.RunUserInputActions(providedInputs, templateJob.Inputs, variables);
176+
177+
if (string.IsNullOrEmpty(FunctionName))
178+
{
179+
FunctionName = providedInputs[GetFunctionNameParamId];
180+
}
181+
182+
await _templatesManager.Deploy(templateJob, template, variables);
183+
}
136184
else
137185
{
138186
SelectionMenuHelper.DisplaySelectionWizardPrompt("template");
@@ -276,7 +324,8 @@ private IEnumerable<string> GetTriggerNames(string templateLanguage, bool forNew
276324

277325
private IEnumerable<Template> GetLanguageTemplates(string templateLanguage, bool forNewModelHelp = false)
278326
{
279-
if (IsNewNodeJsProgrammingModel(workerRuntime) || (forNewModelHelp && (Language == Languages.TypeScript || Language == Languages.JavaScript)))
327+
if (IsNewNodeJsProgrammingModel(workerRuntime) ||
328+
(forNewModelHelp && (Languages.TypeScript.EqualsIgnoreCase(templateLanguage) || Languages.JavaScript.EqualsIgnoreCase(templateLanguage))))
280329
{
281330
return _templates.Value.Where(t => t.Id.EndsWith("-4.x") && t.Metadata.Language.Equals(templateLanguage, StringComparison.OrdinalIgnoreCase));
282331
}
@@ -289,6 +338,21 @@ private IEnumerable<Template> GetLanguageTemplates(string templateLanguage, bool
289338
return _templates.Value.Where(t => t.Metadata.Language.Equals(templateLanguage, StringComparison.OrdinalIgnoreCase));
290339
}
291340

341+
private IEnumerable<string> GetTriggerNamesFromNewTemplates(string templateLanguage, bool forNewModelHelp = false)
342+
{
343+
return GetNewTemplates(templateLanguage, forNewModelHelp).Select(t => t.Name).Distinct();
344+
}
345+
346+
private IEnumerable<NewTemplate> GetNewTemplates(string templateLanguage, bool forNewModelHelp = false)
347+
{
348+
if (IsNewPythonProgrammingModel() || (Languages.Python.EqualsIgnoreCase(templateLanguage) && forNewModelHelp))
349+
{
350+
return _newTemplates.Value.Where(t => t.Language.Equals(templateLanguage, StringComparison.OrdinalIgnoreCase));
351+
}
352+
353+
throw new CliException("The new version of templates are only supported for Python.");
354+
}
355+
292356
private void ConfigureAuthorizationLevel(Template template)
293357
{
294358
var bindings = template.Function["bindings"];
@@ -366,7 +430,7 @@ public async Task<bool> ProcessHelpRequest(string triggerName, bool promptQuesti
366430
return false;
367431
}
368432

369-
var supportedLanguages = new List<string>() { Languages.JavaScript, Languages.TypeScript };
433+
var supportedLanguages = new List<string>() { Languages.JavaScript, Languages.TypeScript, Languages.Python };
370434
if (string.IsNullOrEmpty(Language))
371435
{
372436
if (CurrentPathHasLocalSettings())
@@ -386,8 +450,18 @@ public async Task<bool> ProcessHelpRequest(string triggerName, bool promptQuesti
386450
}
387451
}
388452

389-
var triggerNames = GetTriggerNames(Language, forNewModelHelp: true);
453+
IEnumerable<string> triggerNames;
454+
if (Languages.Python.EqualsIgnoreCase(Language))
455+
{
456+
triggerNames = GetTriggerNamesFromNewTemplates(Language, forNewModelHelp: true);
457+
}
458+
else
459+
{
460+
triggerNames = GetTriggerNames(Language, forNewModelHelp: true);
461+
}
462+
390463
await _contextHelpManager.LoadTriggerHelp(Language, triggerNames.ToList());
464+
391465
if (_contextHelpManager.IsValidTriggerNameForHelp(triggerName))
392466
{
393467
triggerName = _contextHelpManager.GetTriggerTypeFromTriggerNameForHelp(triggerName);
@@ -445,4 +519,4 @@ private bool CurrentPathHasLocalSettings()
445519
return FileSystemHelpers.FileExists(Path.Combine(Environment.CurrentDirectory, "local.settings.json"));
446520
}
447521
}
448-
}
522+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
using Azure.Functions.Cli.Common;
2+
using Azure.Functions.Cli.Helpers;
3+
using Azure.Functions.Cli.Interfaces;
4+
using Colors.Net;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
using System.Text.RegularExpressions;
9+
using static Azure.Functions.Cli.Common.OutputTheme;
10+
using static Azure.Functions.Cli.Common.Constants;
11+
12+
namespace Azure.Functions.Cli.Actions
13+
{
14+
internal interface IUserInputHandler
15+
{
16+
public void RunUserInputActions(IDictionary<string, string> providedValues, IList<TemplateJobInput> inputs, IDictionary<string, string> variables);
17+
public bool ValidateResponse(UserPrompt userPrompt, string response);
18+
public void PrintInputLabel(UserPrompt userPrompt, string defaultValue);
19+
}
20+
21+
internal class UserInputHandler : IUserInputHandler
22+
{
23+
Lazy<IEnumerable<UserPrompt>> _userPrompts;
24+
IDictionary<string, string> _newTemplateLabelMap;
25+
private ITemplatesManager _templatesManager;
26+
27+
public UserInputHandler(ITemplatesManager templatesManager)
28+
{
29+
_templatesManager = templatesManager;
30+
_userPrompts = new Lazy<IEnumerable<UserPrompt>>(() => { return _templatesManager.UserPrompts.Result; });
31+
_newTemplateLabelMap = CreateLabelMap();
32+
}
33+
34+
public void RunUserInputActions(IDictionary<string, string> providedValues, IList<TemplateJobInput> inputs, IDictionary<string, string> variables)
35+
{
36+
foreach (var theInput in inputs)
37+
{
38+
var userPrompt = _userPrompts.Value.First(x => string.Equals(x.Id, theInput.ParamId, StringComparison.OrdinalIgnoreCase));
39+
var defaultValue = theInput.DefaultValue ?? userPrompt.DefaultValue;
40+
string response = null;
41+
if (userPrompt.Value == Constants.UserPromptEnumType || userPrompt.Value == UserPromptBooleanType)
42+
{
43+
var values = new List<string>() { true.ToString(), false.ToString() };
44+
if (userPrompt.Value == UserPromptEnumType)
45+
{
46+
values = userPrompt.EnumList.Select(x => x.Display).ToList();
47+
}
48+
49+
while (!ValidateResponse(userPrompt, response))
50+
{
51+
SelectionMenuHelper.DisplaySelectionWizardPrompt(LabelMap(userPrompt.Label));
52+
response = SelectionMenuHelper.DisplaySelectionWizard(values);
53+
54+
if (string.IsNullOrEmpty(response) && !string.IsNullOrEmpty(defaultValue))
55+
{
56+
response = defaultValue;
57+
}
58+
else if (userPrompt.Value == UserPromptEnumType)
59+
{
60+
response = userPrompt.EnumList.Single(x => x.Display == response).Value;
61+
}
62+
}
63+
}
64+
else
65+
{
66+
// Use the function name if it is already provided by user
67+
if (providedValues.ContainsKey(theInput.ParamId) && !string.IsNullOrEmpty(providedValues[theInput.ParamId]))
68+
{
69+
response = providedValues[theInput.ParamId];
70+
}
71+
72+
while (!ValidateResponse(userPrompt, response))
73+
{
74+
PrintInputLabel(userPrompt, defaultValue);
75+
response = Console.ReadLine();
76+
if (string.IsNullOrEmpty(response) && defaultValue != null)
77+
{
78+
response = defaultValue;
79+
}
80+
}
81+
82+
if (providedValues.ContainsKey(theInput.ParamId))
83+
{
84+
providedValues[theInput.ParamId] = response;
85+
}
86+
}
87+
88+
var variableName = theInput.AssignTo;
89+
variables.Add(variableName, response);
90+
}
91+
}
92+
93+
public bool ValidateResponse(UserPrompt userPrompt, string response)
94+
{
95+
if (response == null)
96+
{
97+
return false;
98+
}
99+
100+
var validator = userPrompt.Validators?.FirstOrDefault();
101+
if (validator == null)
102+
{
103+
return true;
104+
}
105+
106+
var validationRegex = new Regex(validator.Expression);
107+
var isValid = validationRegex.IsMatch(response);
108+
109+
if (!isValid && response != string.Empty)
110+
{
111+
ColoredConsole.WriteLine(ErrorColor($"{this.LabelMap(userPrompt.Label)} is not valid."));
112+
}
113+
114+
return isValid;
115+
}
116+
117+
public void PrintInputLabel(UserPrompt userPrompt, string defaultValue)
118+
{
119+
var label = LabelMap(userPrompt.Label);
120+
ColoredConsole.Write($"{label}: ");
121+
if (!string.IsNullOrEmpty(defaultValue))
122+
{
123+
ColoredConsole.Write($"[{defaultValue}] ");
124+
}
125+
}
126+
127+
private string LabelMap(string label)
128+
{
129+
if (!_newTemplateLabelMap.ContainsKey(label))
130+
return label;
131+
132+
return _newTemplateLabelMap[label];
133+
}
134+
135+
private static IDictionary<string, string> CreateLabelMap()
136+
{
137+
return new Dictionary<string, string>
138+
{
139+
{ "$httpTrigger_route_label", "Route" },
140+
{ "$trigger_functionName_label", "Function Name" },
141+
{ "$app_selected_filename_label", "File Name" },
142+
{ "$httpTrigger_authLevel_label", "Auth Level" },
143+
{ "$queueTrigger_queueName_label", "Queue Name" },
144+
{ "$variables_storageConnStringLabel", "Storage Connection String" },
145+
{ "cosmosDBTrigger-connectionStringSetting", "CosmosDB Connectiong Stirng" },
146+
{ "$cosmosDBIn_databaseName_label", "CosmosDB Database Name" },
147+
{ "$cosmosDBIn_collectionName_label", "CosmosDB Collection Name" },
148+
{ "$cosmosDBIn_leaseCollectionName_label", "CosmosDB Lease Collection Name" },
149+
{ "$cosmosDBIn_createIfNotExists_label", "Create If Not Exists" },
150+
{ "$eventHubTrigger_connection_label", "EventHub Connection" },
151+
{ "$eventHubOut_path_label", "EventHub Out Path" },
152+
{ "$eventHubTrigger_consumerGroup_label", "EventHub Consumer Group" },
153+
{ "$eventHubTrigger_cardinality_label", "EventHub Cardinality" },
154+
{ "$serviceBusTrigger_connection_label", "Service Bus Connection" },
155+
{ "$serviceBusTrigger_queueName_label", "Service Bus Queue Name" },
156+
{ "$serviceBusTrigger_topicName_label", "Service Bus Topic Name" },
157+
{ "$serviceBusTrigger_subscriptionName_label", "Service Bus Subscripton Name" },
158+
{"$timerTrigger_schedule_label", "Schedule" },
159+
};
160+
}
161+
}
162+
}

src/Azure.Functions.Cli/Azure.Functions.Cli.csproj

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
<PublishReadyToRun>false</PublishReadyToRun>
3434
<PublishReadyToRunShowWarnings>false</PublishReadyToRunShowWarnings>
3535
</PropertyGroup>
36+
<ItemGroup>
37+
<None Remove="StaticResources\templatesv2.json" />
38+
</ItemGroup>
3639
<ItemGroup>
3740
<EmbeddedResource Include="StaticResources\bundleConfig.json">
3841
<LogicalName>$(AssemblyName).bundleConfig.json</LogicalName>
@@ -142,8 +145,20 @@
142145
<EmbeddedResource Include="StaticResources\local.settings.json.template">
143146
<LogicalName>$(AssemblyName).local.settings.json</LogicalName>
144147
</EmbeddedResource>
148+
<EmbeddedResource Include="StaticResources\NewTemplate-userPrompts.json">
149+
<LogicalName>$(AssemblyName).NewTemplate-userPrompts.json</LogicalName>
150+
</EmbeddedResource>
145151
<EmbeddedResource Include="StaticResources\node-v4-templates.json">
146152
<LogicalName>$(AssemblyName).node-v4-templates.json</LogicalName>
153+
</EmbeddedResource>
154+
<EmbeddedResource Include="StaticResources\Python-HttpTrigger-help.txt">
155+
<LogicalName>$(AssemblyName).python-HttpTrigger-help.txt</LogicalName>
156+
</EmbeddedResource>
157+
<EmbeddedResource Include="StaticResources\Python-TimerTrigger-help.txt">
158+
<LogicalName>$(AssemblyName).python-TimerTrigger-help.txt</LogicalName>
159+
</EmbeddedResource>
160+
<EmbeddedResource Include="StaticResources\templatesv2.json">
161+
<LogicalName>$(AssemblyName).templatesv2.json</LogicalName>
147162
</EmbeddedResource>
148163
<EmbeddedResource Include="StaticResources\Typescript-BlobTrigger-help.txt">
149164
<LogicalName>$(AssemblyName).typescript-BlobTrigger-help.txt</LogicalName>

0 commit comments

Comments
 (0)