Skip to content

Commit 61bb43d

Browse files
committedDec 9, 2019
initial commit
1 parent cf877ee commit 61bb43d

File tree

9 files changed

+432
-35
lines changed

9 files changed

+432
-35
lines changed
 

‎.gitignore

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
55

66
# User-specific files
7-
*.rsuser
87
*.suo
98
*.user
109
*.userosscache
@@ -13,23 +12,17 @@
1312
# User-specific files (MonoDevelop/Xamarin Studio)
1413
*.userprefs
1514

16-
# Mono auto generated files
17-
mono_crash.*
18-
1915
# Build results
2016
[Dd]ebug/
2117
[Dd]ebugPublic/
2218
[Rr]elease/
2319
[Rr]eleases/
2420
x64/
2521
x86/
26-
[Aa][Rr][Mm]/
27-
[Aa][Rr][Mm]64/
2822
bld/
2923
[Bb]in/
3024
[Oo]bj/
3125
[Ll]og/
32-
[Ll]ogs/
3326

3427
# Visual Studio 2015/2017 cache/options directory
3528
.vs/
@@ -43,10 +36,9 @@ Generated\ Files/
4336
[Tt]est[Rr]esult*/
4437
[Bb]uild[Ll]og.*
4538

46-
# NUnit
39+
# NUNIT
4740
*.VisualState.xml
4841
TestResult.xml
49-
nunit-*.xml
5042

5143
# Build Results of an ATL Project
5244
[Dd]ebugPS/
@@ -60,14 +52,15 @@ BenchmarkDotNet.Artifacts/
6052
project.lock.json
6153
project.fragment.lock.json
6254
artifacts/
55+
**/Properties/launchSettings.json
6356

6457
# StyleCop
6558
StyleCopReport.xml
6659

6760
# Files built by Visual Studio
6861
*_i.c
6962
*_p.c
70-
*_h.h
63+
*_i.h
7164
*.ilk
7265
*.meta
7366
*.obj
@@ -84,7 +77,6 @@ StyleCopReport.xml
8477
*.tlh
8578
*.tmp
8679
*.tmp_proj
87-
*_wpftmp.csproj
8880
*.log
8981
*.vspscc
9082
*.vssscc
@@ -127,6 +119,9 @@ _ReSharper*/
127119
*.[Rr]e[Ss]harper
128120
*.DotSettings.user
129121

122+
# JustCode is a .NET coding add-in
123+
.JustCode
124+
130125
# TeamCity is a build add-in
131126
_TeamCity*
132127

@@ -184,8 +179,6 @@ PublishScripts/
184179

185180
# NuGet Packages
186181
*.nupkg
187-
# NuGet Symbol Packages
188-
*.snupkg
189182
# The packages folder can be ignored because of Package Restore
190183
**/[Pp]ackages/*
191184
# except build/, which is used as an MSBuild target.
@@ -210,14 +203,12 @@ BundleArtifacts/
210203
Package.StoreAssociation.xml
211204
_pkginfo.txt
212205
*.appx
213-
*.appxbundle
214-
*.appxupload
215206

216207
# Visual Studio cache files
217208
# files ending in .cache can be ignored
218209
*.[Cc]ache
219210
# but keep track of directories ending in .cache
220-
!?*.[Cc]ache/
211+
!*.[Cc]ache/
221212

222213
# Others
223214
ClientBin/
@@ -230,7 +221,7 @@ ClientBin/
230221
*.publishsettings
231222
orleans.codegen.cs
232223

233-
# Including strong name files can present a security risk
224+
# Including strong name files can present a security risk
234225
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
235226
#*.snk
236227

@@ -261,9 +252,6 @@ ServiceFabricBackup/
261252
*.bim.layout
262253
*.bim_*.settings
263254
*.rptproj.rsuser
264-
*- [Bb]ackup.rdl
265-
*- [Bb]ackup ([0-9]).rdl
266-
*- [Bb]ackup ([0-9][0-9]).rdl
267255

268256
# Microsoft Fakes
269257
FakesAssemblies/
@@ -299,8 +287,12 @@ paket-files/
299287
# FAKE - F# Make
300288
.fake/
301289

302-
# CodeRush personal settings
303-
.cr/personal
290+
# JetBrains Rider
291+
.idea/
292+
*.sln.iml
293+
294+
# CodeRush
295+
.cr/
304296

305297
# Python Tools for Visual Studio (PTVS)
306298
__pycache__/
@@ -325,7 +317,7 @@ __pycache__/
325317
# OpenCover UI analysis results
326318
OpenCover/
327319

328-
# Azure Stream Analytics local run output
320+
# Azure Stream Analytics local run output
329321
ASALocalRun/
330322

331323
# MSBuild Binary and Structured Log
@@ -334,17 +326,10 @@ ASALocalRun/
334326
# NVidia Nsight GPU debugger configuration file
335327
*.nvuser
336328

337-
# MFractors (Xamarin productivity tool) working folder
329+
# MFractors (Xamarin productivity tool) working folder
338330
.mfractor/
339331

340-
# Local History for Visual Studio
341-
.localhistory/
342-
343-
# BeatPulse healthcheck temp database
344-
healthchecksdb
345-
346-
# Backup folder for Package Reference Convert tool in Visual Studio 2017
347-
MigrationBackup/
348-
349-
# Ionide (cross platform F# VS Code tools) working folder
350-
.ionide/
332+
# Additional Files
333+
venv/
334+
env/
335+
env.*

‎.vscode/launch.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "Python: Flask",
9+
"type": "python",
10+
"request": "launch",
11+
"module": "flask",
12+
"env": {
13+
"FLASK_APP": "app.py",
14+
"FLASK_ENV": "development",
15+
"FLASK_DEBUG": "0"
16+
},
17+
"args": [
18+
"run",
19+
"--no-debugger",
20+
"--no-reload"
21+
],
22+
"jinja": true
23+
}
24+
]
25+
}

‎.vscode/settings.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"python.pythonPath": "venv\\Scripts\\python.exe",
3+
"cSpell.words": [
4+
"pyodbc"
5+
]
6+
}

‎app.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import sys
2+
import os
3+
from flask import Flask
4+
from flask_restful import reqparse, abort, Api, Resource
5+
import json
6+
import pyodbc
7+
8+
# Initialize Flask
9+
app = Flask(__name__)
10+
11+
# Setup Flask Restful framework
12+
api = Api(app)
13+
parser = reqparse.RequestParser()
14+
parser.add_argument('customer')
15+
16+
# Create connection to Azure SQL
17+
conn = pyodbc.connect(os.environ['SQLAZURECONNSTR_WWIF'])
18+
19+
class Queryable(Resource):
20+
def executeQueryJson(self, verb, payload=None):
21+
result = {}
22+
cursor = conn.cursor()
23+
entity = type(self).__name__.lower()
24+
procedure = f"web.{verb}_{entity}"
25+
try:
26+
if payload:
27+
cursor.execute(f"EXEC {procedure} ?", json.dumps(payload))
28+
else:
29+
cursor.execute(f"EXEC {procedure}")
30+
31+
result = cursor.fetchone()
32+
33+
if result:
34+
result = json.loads(result[0])
35+
else:
36+
result = {}
37+
38+
cursor.commit()
39+
except:
40+
print("Unexpected error:", sys.exc_info()[0])
41+
raise
42+
finally:
43+
cursor.close()
44+
45+
return result
46+
47+
# Customer Class
48+
class Customer(Queryable):
49+
def get(self, customer_id):
50+
customer = {}
51+
customer["CustomerID"] = customer_id
52+
result = self.executeQueryJson("get", customer)
53+
return result, 200
54+
55+
def put(self):
56+
args = parser.parse_args()
57+
customer = json.loads(args['customer'])
58+
result = self.executeQueryJson("put", customer)
59+
return result, 201
60+
61+
def patch(self, customer_id):
62+
args = parser.parse_args()
63+
customer = json.loads(args['customer'])
64+
customer["CustomerID"] = customer_id
65+
result = self.executeQueryJson("patch", customer)
66+
return result, 202
67+
68+
def delete(self, customer_id):
69+
customer = {}
70+
customer["CustomerID"] = customer_id
71+
result = self.executeQueryJson("delete", customer)
72+
return result, 202
73+
74+
# Customers Class
75+
class Customers(Queryable):
76+
def get(self):
77+
result = self.executeQueryJson("get")
78+
return result, 200
79+
80+
81+
# Create API routes
82+
api.add_resource(Customer, '/customer', '/customer/<customer_id>')
83+
api.add_resource(Customers, '/customers')

‎azure-deploy.sh

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/bin/bash
2+
3+
set -euo pipefail
4+
5+
# Make sure these values are correct for your environment
6+
resourceGroup="azure-sql-db-python-rest-api"
7+
appName="azure-sql-db-python-rest-api"
8+
location="WestUS2"
9+
10+
# Change this if you are using your own github repository
11+
gitSource="https://github.com/yorek/azure-sql-db-python-rest-api.git"
12+
13+
az group create \
14+
-n $resourceGroup \
15+
-l $location
16+
17+
az appservice plan create \
18+
-g $resourceGroup \
19+
-n "linux-plan" \
20+
--sku B1 \
21+
--is-linux
22+
23+
az webapp create \
24+
-g $resourceGroup \
25+
-n $appName \
26+
--plan "linux-plan" \
27+
--runtime "Python|3.7" \
28+
--deployment-source-url $gitSource \
29+
--deployment-source-branch master
30+
31+
az webapp config connection-string set \
32+
-g $resourceGroup \
33+
-n $appName \
34+
--settings WWIF=$SQLAZURECONNSTR_WWIF
35+
--connection-string-type=SQLAzure

‎requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pyodbc
2+
flask
3+
flask-restful

‎sample-usage.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Sample REST API usage with cUrl
2+
3+
## Get a customer
4+
5+
```bash
6+
curl -s -X GET http://localhost:5000/customer/123
7+
```
8+
9+
## Create new customer
10+
11+
```bash
12+
curl -s -X PUT http://localhost:5000/customer -d 'customer={"CustomerName": "John Doe", "PhoneNumber": "123-234-5678", "FaxNumber": "123-234-5678", "WebsiteURL": "http://www.something.com", "Delivery": { "AddressLine1": "One Microsoft Way", "PostalCode": 98052 }}'
13+
```
14+
15+
## Update customer
16+
17+
```bash
18+
curl -s -X PATCH http://localhost:5000/customer/123 -d 'customer={"CustomerName": "Jane Dean", "PhoneNumber": "231-778-5678" }'
19+
```
20+
21+
## Delete a customer
22+
23+
```bash
24+
curl -s -X DELETE http://localhost:5000/customer/123
25+
```

‎simple-app.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import sys
2+
import os
3+
from flask import Flask
4+
from flask_restful import reqparse, Api, Resource
5+
import json
6+
import pyodbc
7+
8+
# This is a simplified example that only support GET request.
9+
# It is meant to help you to get you started if you're new to development
10+
# and to show how simple is using Azure SQL with Python
11+
# A more complete example is in "app.py"
12+
# To run this simplified sample follow the README, but instead of running "flask run"
13+
# just run "python ./simple-app.py"
14+
# Enjoy!
15+
16+
# Initialize Flask
17+
app = Flask(__name__)
18+
19+
# Setup Flask Restful framework
20+
api = Api(app)
21+
parser = reqparse.RequestParser()
22+
parser.add_argument('customer')
23+
24+
# Create connection to Azure SQL
25+
conn = pyodbc.connect(os.environ['SQLAZURECONNSTR_WWIF'])
26+
27+
# Customer Class
28+
class Customer(Resource):
29+
def get(self, customer_id):
30+
customer = {"CustomerID": customer_id}
31+
cursor = conn.cursor()
32+
cursor.execute("EXEC web.get_customer ?", json.dumps(customer))
33+
result = json.loads(cursor.fetchone()[0])
34+
cursor.close()
35+
return result, 200
36+
37+
# Create API route to defined Customer class
38+
api.add_resource(Customer, '/customer', '/customer/<customer_id>')
39+
40+
# Start App
41+
if __name__ == '__main__':
42+
app.run()

‎sql/WideWorldImportersUpdates.sql

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/*
2+
Create schema
3+
*/
4+
IF SCHEMA_ID('web') IS NULL BEGIN
5+
EXECUTE('CREATE SCHEMA [web]');
6+
END
7+
GO
8+
9+
/*
10+
Create user to be used in the sample API solution
11+
*/
12+
IF USER_ID('PythonWebApp') IS NULL BEGIN
13+
CREATE USER [PythonWebApp] WITH PASSWORD = 'a987REALLY#$%TRONGpa44w0rd';
14+
END
15+
16+
/*
17+
Grant execute permission to created users
18+
*/
19+
GRANT EXECUTE ON SCHEMA::[web] TO [PythonWebApp];
20+
GO
21+
22+
/*
23+
Return details on a specific customer
24+
*/
25+
CREATE OR ALTER PROCEDURE web.get_customer
26+
@Json NVARCHAR(MAX)
27+
AS
28+
SET NOCOUNT ON;
29+
DECLARE @CustomerId INT = JSON_VALUE(@Json, '$.CustomerID');
30+
SELECT
31+
[CustomerID],
32+
[CustomerName],
33+
[PhoneNumber],
34+
[FaxNumber],
35+
[WebsiteURL],
36+
[DeliveryAddressLine1] AS 'Delivery.AddressLine1',
37+
[DeliveryAddressLine2] AS 'Delivery.AddressLine2',
38+
[DeliveryPostalCode] AS 'Delivery.PostalCode'
39+
FROM
40+
[Sales].[Customers]
41+
WHERE
42+
[CustomerID] = @CustomerId
43+
FOR JSON PATH
44+
GO
45+
46+
/*
47+
Delete a specific customer
48+
*/
49+
CREATE OR ALTER PROCEDURE web.delete_customer
50+
@Json NVARCHAR(MAX)
51+
AS
52+
SET NOCOUNT ON;
53+
DECLARE @CustomerId INT = JSON_VALUE(@Json, '$.CustomerID');
54+
DELETE FROM [Sales].[Customers] WHERE CustomerId = @CustomerId;
55+
SELECT * FROM (SELECT CustomerID = @CustomerId) D FOR JSON AUTO;
56+
GO
57+
58+
/*
59+
Update (Patch) a specific customer
60+
*/
61+
CREATE OR ALTER PROCEDURE web.patch_customer
62+
@Json NVARCHAR(MAX)
63+
AS
64+
SET NOCOUNT ON;
65+
DECLARE @CustomerId INT = JSON_VALUE(@Json, '$.CustomerID');
66+
WITH [source] AS
67+
(
68+
SELECT * FROM OPENJSON(@Json) WITH (
69+
[CustomerID] INT,
70+
[CustomerName] NVARCHAR(100),
71+
[PhoneNumber] NVARCHAR(20),
72+
[FaxNumber] NVARCHAR(20),
73+
[WebsiteURL] NVARCHAR(256),
74+
[DeliveryAddressLine1] NVARCHAR(60) '$.Delivery.AddressLine1',
75+
[DeliveryAddressLine2] NVARCHAR(60) '$.Delivery.AddressLine2',
76+
[DeliveryPostalCode] NVARCHAR(10) '$.Delivery.PostalCode'
77+
)
78+
)
79+
UPDATE
80+
t
81+
SET
82+
t.[CustomerName] = COALESCE(s.[CustomerName], t.[CustomerName]),
83+
t.[PhoneNumber] = COALESCE(s.[PhoneNumber], t.[PhoneNumber]),
84+
t.[FaxNumber] = COALESCE(s.[FaxNumber], t.[FaxNumber]),
85+
t.[WebsiteURL] = COALESCE(s.[WebsiteURL], t.[WebsiteURL]),
86+
t.[DeliveryAddressLine1] = COALESCE(s.[DeliveryAddressLine1], t.[DeliveryAddressLine1]),
87+
t.[DeliveryAddressLine2] = COALESCE(s.[DeliveryAddressLine2], t.[DeliveryAddressLine2]),
88+
t.[DeliveryPostalCode] = COALESCE(s.[DeliveryPostalCode], t.[DeliveryPostalCode])
89+
FROM
90+
[Sales].[Customers] t
91+
INNER JOIN
92+
[source] s ON t.[CustomerID] = s.[CustomerID]
93+
WHERE
94+
t.CustomerId = @CustomerId;
95+
96+
DECLARE @Json2 NVARCHAR(MAX) = N'{"CustomerID": ' + CAST(@CustomerId AS NVARCHAR(9)) + N'}'
97+
EXEC web.get_customer @Json2;
98+
GO
99+
100+
/*
101+
Create a new customer
102+
*/
103+
104+
CREATE OR ALTER PROCEDURE web.put_customer
105+
@Json NVARCHAR(MAX)
106+
AS
107+
SET NOCOUNT ON;
108+
DECLARE @CustomerId INT = NEXT VALUE FOR Sequences.CustomerID;
109+
WITH [source] AS
110+
(
111+
SELECT * FROM OPENJSON(@Json) WITH (
112+
[CustomerName] NVARCHAR(100),
113+
[PhoneNumber] NVARCHAR(20),
114+
[FaxNumber] NVARCHAR(20),
115+
[WebsiteURL] NVARCHAR(256),
116+
[DeliveryAddressLine1] NVARCHAR(60) '$.Delivery.AddressLine1',
117+
[DeliveryAddressLine2] NVARCHAR(60) '$.Delivery.AddressLine2',
118+
[DeliveryPostalCode] NVARCHAR(10) '$.Delivery.PostalCode'
119+
)
120+
)
121+
INSERT INTO [Sales].[Customers]
122+
(
123+
CustomerID,
124+
CustomerName,
125+
BillToCustomerID,
126+
CustomerCategoryID,
127+
PrimaryContactPersonID,
128+
DeliveryMethodID,
129+
DeliveryCityID,
130+
PostalCityID,
131+
AccountOpenedDate,
132+
StandardDiscountPercentage,
133+
IsStatementSent,
134+
IsOnCreditHold,
135+
PaymentDays,
136+
PhoneNumber,
137+
FaxNumber,
138+
WebsiteURL,
139+
DeliveryAddressLine1,
140+
DeliveryAddressLine2,
141+
DeliveryPostalCode,
142+
PostalAddressLine1,
143+
PostalAddressLine2,
144+
PostalPostalCode,
145+
LastEditedBy
146+
)
147+
SELECT
148+
@CustomerId,
149+
CustomerName,
150+
@CustomerId,
151+
5, -- Computer Shop
152+
1, -- No contact person
153+
1, -- Post Delivery
154+
28561, -- Redmond
155+
28561, -- Redmond
156+
SYSUTCDATETIME(),
157+
0.00,
158+
0,
159+
0,
160+
30,
161+
PhoneNumber,
162+
FaxNumber,
163+
WebsiteURL,
164+
DeliveryAddressLine1,
165+
DeliveryAddressLine2,
166+
DeliveryPostalCode,
167+
DeliveryAddressLine1,
168+
DeliveryAddressLine2,
169+
DeliveryPostalCode,
170+
1
171+
FROM
172+
[source]
173+
;
174+
175+
DECLARE @Json2 NVARCHAR(MAX) = N'{"CustomerID": ' + CAST(@CustomerId AS NVARCHAR(9)) + N'}'
176+
EXEC web.get_customer @Json2;
177+
GO
178+
179+
CREATE OR ALTER PROCEDURE web.get_customers
180+
AS
181+
SET NOCOUNT ON;
182+
-- Cast is needed to corretly inform pyodbc of output type is NVARCHAR(MAX)
183+
-- Needed if generated json is bigger then 4000 bytes and thus pyodbc trucates it
184+
-- https://stackoverflow.com/questions/49469301/pyodbc-truncates-the-response-of-a-sql-server-for-json-query
185+
SELECT CAST((
186+
SELECT
187+
[CustomerID],
188+
[CustomerName]
189+
FROM
190+
[Sales].[Customers]
191+
FOR JSON PATH) AS NVARCHAR(MAX)) AS JsonResult
192+
GO
193+

0 commit comments

Comments
 (0)
Please sign in to comment.