<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Python - Alexander Development]]></title><description><![CDATA[Python - Alexander Development]]></description><link>https://alexanderdevelopment.net/</link><image><url>https://alexanderdevelopment.net/favicon.png</url><title>Python - Alexander Development</title><link>https://alexanderdevelopment.net/</link></image><generator>Ghost 1.20</generator><lastBuildDate>Mon, 24 Aug 2020 14:28:21 GMT</lastBuildDate><atom:link href="https://alexanderdevelopment.net/tag/python/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[A motion-activated spider for Halloween with an Arduino and a Raspberry Pi]]></title><description><![CDATA[<div class="kg-card-markdown"><p>Every year my family hangs a large decorative spider over our doorway for Halloween. This year I decided to make it flash red LED eyes and play a random assortment of spooky sounds (including, but not limited to, bats, chains and the Vincent Price laugh from &quot;Thriller&quot;) when</p></div>]]></description><link>https://alexanderdevelopment.net/post/2018/10/26/a-motion-activated-spider-for-halloween-with-an-arduino-and-a-raspberry-pi/</link><guid isPermaLink="false">5bd2522e14b5e0000112ec57</guid><category><![CDATA[Raspberry Pi]]></category><category><![CDATA[Arduino]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Lucas Alexander]]></dc:creator><pubDate>Fri, 26 Oct 2018 14:11:53 GMT</pubDate><media:content url="https://alexanderdevelopment.net/content/images/2018/10/candy-corn.jpg" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><img src="https://alexanderdevelopment.net/content/images/2018/10/candy-corn.jpg" alt="A motion-activated spider for Halloween with an Arduino and a Raspberry Pi"><p>Every year my family hangs a large decorative spider over our doorway for Halloween. This year I decided to make it flash red LED eyes and play a random assortment of spooky sounds (including, but not limited to, bats, chains and the Vincent Price laugh from &quot;Thriller&quot;) when trick-or-treaters come to the door. In today's blog post, I'll show how I did it.</p>
<h4 id="theapproach">The approach</h4>
<p>A few years ago, I wrote a post about setting up a <a href="https://alexanderdevelopment.net/post/2016/01/18/dynamics-crm-and-the-internet-of-things-part-5/">motion-activated webcam to trigger license plate recognition with a Raspberry Pi</a>, so I already had a Raspberry Pi and an HC-SR04 ultrasonic rangefinder I could use to detect motion. I initially thought I would only need the Pi for this project, but as I started evaluating the physical constraints of my porch, I realized it would be difficult to mount the Pi in a secure location where it could handle motion detection and also flash the LED eyes. I decided a better approach would be to use an Arduino to detect motion and flash the LEDs, while the Pi would communicate with the Arduino over a serial connection and play Halloween sound effect MP3s.</p>
<h4 id="settingupthearduino">Setting up the Arduino</h4>
<p>I have two red LEDs taped over the spider's eyes that are connected in series to pin 12 on an Arduino UNO. The ultrasonic rangefinder is taped to the side of my porch so that trick-or-treaters have to pass by it before they can knock on my door, and it is connected to pin 9 for the trigger and pin 10 for the echo. The Arduino listens for serial commands from the Pi to measure distance or to flash the LEDs if the Pi determines the reported distance measurement means someone has approached.</p>
<p>Here's my sketch:</p>
<pre><code>//set the pin numbers
const int triggerPin = 9;
const int echoPin = 10;
const int ledPin = 12;

void setup() 
{
  Serial.begin(9600); // Starts the serial communication
  pinMode(triggerPin, OUTPUT); // Sets the triggerPin as an Output
  pinMode(echoPin, INPUT); // Sets the echoPin as an Input
  pinMode(ledPin, OUTPUT);
}

void loop() 
{
  if (Serial.available() &gt; 0) { 
    int controlcode = Serial.parseInt();
    if(controlcode==1)
    {
      //clear the trigger pin
      digitalWrite(triggerPin, LOW);
      delayMicroseconds(2);

      //send a 10 microsecond pulse
      digitalWrite(triggerPin, HIGH);
      delayMicroseconds(10);
      digitalWrite(triggerPin, LOW);

      //read the echo pin to get the duration in microseconds
      long duration = pulseIn(echoPin, HIGH);
      
      //calculate the distance in centimeters
      int distance = duration*0.034/2;
      
      //return the distance to the serial monitor
      Serial.println(distance);
    }
    if(controlcode==2)
    {
      flashEyes();
    }
  }
}

void flashEyes()
{
  //flash 50 times
  for(int i=0;i&lt;50;i++)
  {
    //turn the eyes on
    digitalWrite(ledPin, HIGH);
    
    //wait for 150ms
    delay(150);
    
    //turn the eyes off
    digitalWrite(ledPin, LOW);
    
    //wait for 100ms
    delay(100);
  }
}
</code></pre>
<h4 id="settinguptheraspberrypi">Setting up the Raspberry Pi</h4>
<p>My Raspberry Pi 2 Model B is connected to the Arduino over a standard USB A/B cable, and it's also connected to a Bluetooth speaker mounted behind the spider. To run things, I have a Python script that sends a serial command to the Arduino to measure the distance every 50 milliseconds. If the Arduino returns a value between 20 and 120 centimeters, the Python script sends a serial command to the Arduino to flash the LEDs, and uses <a href="https://linux.die.net/man/1/mpg123">mpg123</a> to play a random MP3 file.</p>
<p>Here's the script:</p>
<pre><code>import serial
import time
import subprocess
import random
port = &quot;/dev/ttyACM0&quot;

def playsound():
    i = 0
    screamnumber = str(random.randint(1,5))
    screamfile = &quot;/home/pi/halloween/X.mp3&quot;.replace(&quot;X&quot;,screamnumber) 
    subprocess.Popen([&quot;mpg123&quot;, screamfile])
 
if __name__ == '__main__':
    s1 = serial.Serial(
      port=port,\
      baudrate=9600)
    s1.flushInput()
    
    #wait 5 seconds before telling the arduino look for motion
    time.sleep(5)
    try:
        while True:
          if s1.in_waiting==0:
            #print('sent request')
            s1.write('1\n')
            time.sleep(.05)
          if s1.in_waiting&gt;0:
            #print('received response')
            inputValue = s1.readline()
            #print('Distance: ' + inputValue)
            if int(inputValue) &gt; 20 and int(inputValue) &lt; 120:
              print('Distance: ' + inputValue)
              s1.write('2\n')
              playsound()
              time.sleep(15)
            else:
              time.sleep(.05)
 
    #quit with ctrl + c
    except KeyboardInterrupt:
        print(&quot;script stopped by user&quot;)
</code></pre>
<p>To enable serial communication, I am using the <a href="https://github.com/pyserial/pyserial">pySerial</a> module. I should also note the logic for playing a random MP3 assumes that there are five files in the same directory as the script that are named 1.mp3, 2.mp3, etc. Using a different number of files would require changing the upper bound value on line 9.</p>
<p>Happy Halloween!</p>
</div>]]></content:encoded></item><item><title><![CDATA[An Azure AD OAuth 2 helper microservice]]></title><description><![CDATA[<div class="kg-card-markdown"><p>One of the biggest trends in systems architecture these days is the use of &quot;serverless&quot; functions like Azure Functions, Amazon Lambda and OpenFaas. Because these functions are stateless, if you want to use a purely serverless approach to work with resources secured using Azure Active Directory like Dynamics</p></div>]]></description><link>https://alexanderdevelopment.net/post/2018/05/19/an-azure-ad-oauth2-helper-microservice/</link><guid isPermaLink="false">5aff468b97f5e30001931b5d</guid><category><![CDATA[Microsoft Dynamics CRM]]></category><category><![CDATA[Dynamics 365]]></category><category><![CDATA[Python]]></category><category><![CDATA[serverless]]></category><category><![CDATA[Docker]]></category><dc:creator><![CDATA[Lucas Alexander]]></dc:creator><pubDate>Sat, 19 May 2018 16:45:38 GMT</pubDate><media:content url="https://alexanderdevelopment.net/content/images/2018/05/Postman_2018-05-18_22-58-04-1.png" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><img src="https://alexanderdevelopment.net/content/images/2018/05/Postman_2018-05-18_22-58-04-1.png" alt="An Azure AD OAuth 2 helper microservice"><p>One of the biggest trends in systems architecture these days is the use of &quot;serverless&quot; functions like Azure Functions, Amazon Lambda and OpenFaas. Because these functions are stateless, if you want to use a purely serverless approach to work with resources secured using Azure Active Directory like Dynamics 365 online, a new token will have to be requested every time a function executes. This is inefficient, and it requires the function to fully understand OAuth 2 authentication, which could be handled better elsewhere.</p>
<p>To address this problem, I've written a microservice in Python that can be used to request OAuth 2 tokens from Azure Active Directory, and it also handles refreshing them as needed. I've containerized it as Docker image so you can easily run it without needing to build anything.</p>
<h4 id="howitworks">How it works</h4>
<p>When a request containing a username and password arrives for the first time, the microservice retrieves an OAuth2 access token from Azure AD and returns it to the requester. The microservice also caches an object that contains the access token, refresh token, username, password and expiration time.</p>
<p>When subsequent requests arrive, the microservice checks its cache for an existing token that matches the username and password. If it finds one, it checks if the token has expired or needs to be refreshed.</p>
<p>If the existing token has expired, a new one is requested. If the existing token has not expired, but it will expire within a specified period of time (10 minutes is the default value), the microservice will execute a refresh request to Azure AD, cache the updated token and return it to the requester. If there's an unexpired existing token that doesn't need to be refreshed, the cached access token will be returned to the requester.</p>
<p>Here's what a raw token request to and response from the microservice looks like in Postman:<br>
<img src="https://alexanderdevelopment.net/content/images/2018/05/Postman_2018-05-18_22-58-04.png#img-thumbnail" alt="An Azure AD OAuth 2 helper microservice"></p>
<p>Back in 2016 I shared <a href="https://alexanderdevelopment.net/post/2016/11/27/dynamics-365-and-python-integration-using-the-web-api/">some sample Python code</a> that showed how to authenticate to Azure AD and query the Dynamics 365 (then called Dynamics CRM) Web API. Here is an updated version of that sample code that uses this new microservice to acquire access tokens:</p>
<pre><code>import requests
import json

#set these values to retrieve the oauth token
username = 'lucasalexander@xxxxxx.onmicrosoft.com'
userpassword = 'xxxxxx'
tokenendpoint = 'http://localhost:5000/requesttoken'

#set these values to query your crm data
crmwebapi = 'https://xxxxxx.api.crm.dynamics.com/api/data/v8.2'
crmwebapiquery = '/contacts?$select=fullname,contactid'

#build the authorization request
tokenpost = {
    'username':username,
    'password':userpassword
}

#make the token request
print('requesting token . . .')
tokenres = requests.post(tokenendpoint, json=tokenpost)
print('token response received. . .')

accesstoken = ''

#extract the access token
try:
    print('parsing token response . . .')
    print(tokenres)
    accesstoken = tokenres.json()['accesstoken']

except(KeyError):
    print('Could not get access token')

if(accesstoken!=''):
    crmrequestheaders = {
        'Authorization': 'Bearer ' + accesstoken,
        'OData-MaxVersion': '4.0',
        'OData-Version': '4.0',
        'Accept': 'application/json',
        'Content-Type': 'application/json; charset=utf-8',
        'Prefer': 'odata.maxpagesize=500',
        'Prefer': 'odata.include-annotations=OData.Community.Display.V1.FormattedValue'
    }

    print('making crm request . . .')
    crmres = requests.get(crmwebapi+crmwebapiquery, headers=crmrequestheaders)
    print('crm response received . . .')
    try:
        print('parsing crm response . . .')
        crmresults = crmres.json()
        for x in crmresults['value']:
            print (x['fullname'] + ' - ' + x['contactid'])
    except KeyError:
        print('Could not parse CRM results')
</code></pre>
<p>Here's the output when I run the sample against my demo environment:<br>
<img src="https://alexanderdevelopment.net/content/images/2018/05/powershell_2018-05-19_11-34-16.png" alt="An Azure AD OAuth 2 helper microservice"></p>
<h4 id="runningthemicroservice">Running the microservice</h4>
<p>Pull the image from <a href="https://hub.docker.com">Docker Hub</a>: <code>docker pull lucasalexander/azuread-oauth2-helper:latest</code></p>
<p><em>Required environment variables</em></p>
<ol>
<li>RESOURCE - The URL of the service that is going to be accessed</li>
<li>CLIENTID - The Azure AD application client ID</li>
<li>TOKEN_ENDPOINT - The OAuth2 token endpoint from the Azure AD application</li>
</ol>
<p>Run the image with the following command (replacing the environment variables with your own).</p>
<p><code>docker run -d -p 5000:5000 -e RESOURCE=https://XXXXXX.crm.dynamics.com -e CLIENTID=XXXXXX -e TOKEN_ENDPOINT=https://login.microsoftonline.com/XXXXXX/oauth2/token --name oauthhelper lucasalexander/azuread-oauth2-helper:latest</code></p>
<p>You can also optionally supply an additional &quot;REFRESH_THRESHOLD&quot; environment variable that sets the time remaining (in seconds) before a token's expiration time when it will be refreshed. The default value is 600 seconds.</p>
<h4 id="anoteonsecurity">A note on security</h4>
<p>Because the microservice is caching usernames, passwords and access tokens in memory, this approach is vulnerable to heap inspection attacks, so you'll want to make sure your environment is appropriately locked down. Also you'll want to make sure any communication between your code that requests tokens and the microservice is encrypted.</p>
<h4 id="wrappingup">Wrapping up</h4>
<p>Although I wrote this with Dynamics 365 in mind, it should work for any resource that is secured by Azure AD. If you'd like to take a closer look at the code, it's available on GitHub <a href="https://github.com/lucasalexander/azuread-oauth2-helper">here</a>.</p>
</div>]]></content:encoded></item><item><title><![CDATA[Building a simple service relay for Dynamics 365 CE with RabbitMQ and Python - part 4]]></title><description><![CDATA[<div class="kg-card-markdown"><p>This is the final post in my series about building a service relay for Dynamics 365 CE with RabbitMQ and Python. In my previous <a href="https://alexanderdevelopment.net/post/2018/02/05/building-a-simple-service-relay-for-dynamics-365-ce-with-rabbitmq-and-python-part-3/">post</a> in this series, I showed the Python code to make the service relay work. In today's post, I will show how you can use <a href="https://azure.microsoft.com/en-us/services/functions/">Azure</a></p></div>]]></description><link>https://alexanderdevelopment.net/post/2018/02/07/building-a-simple-service-relay-for-dynamics-365-ce-with-rabbitmq-and-python-part-4/</link><guid isPermaLink="false">5a788a53c86c8900016cf367</guid><category><![CDATA[Microsoft Dynamics CRM]]></category><category><![CDATA[Dynamics 365]]></category><category><![CDATA[integration]]></category><category><![CDATA[Python]]></category><category><![CDATA[RabbitMQ]]></category><category><![CDATA[Azure]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Lucas Alexander]]></dc:creator><pubDate>Thu, 08 Feb 2018 04:00:42 GMT</pubDate><media:content url="https://alexanderdevelopment.net/content/images/2018/02/simple-service-relay-2.png" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><img src="https://alexanderdevelopment.net/content/images/2018/02/simple-service-relay-2.png" alt="Building a simple service relay for Dynamics 365 CE with RabbitMQ and Python - part 4"><p>This is the final post in my series about building a service relay for Dynamics 365 CE with RabbitMQ and Python. In my previous <a href="https://alexanderdevelopment.net/post/2018/02/05/building-a-simple-service-relay-for-dynamics-365-ce-with-rabbitmq-and-python-part-3/">post</a> in this series, I showed the Python code to make the service relay work. In today's post, I will show how you can use <a href="https://azure.microsoft.com/en-us/services/functions/">Azure Functions</a> to make a consumer service proxy using C# so client applications don't have to access to your RabbitMQ broker directly, and I will also discuss some general thoughts on security and scalability for this service relay architecture.</p>
<p>Although this simple service relay allows external consumers to get data from Dynamics 365 CE without needing to connect directly, the examples I've shown so far require that they can connect to a RabbitMQ broker. This may be problematic for a variety of reasons, so you would probably want external consumers to connect to a web service proxy that would write requests to and read responses from the RabbitMQ broker.</p>
<h4 id="buildingaserviceproxyfunction">Building a service proxy function</h4>
<p>You can build an Azure Functions service proxy with Python, but I don't recommend it for three reasons:</p>
<ol>
<li>Azure Functions Python support is still considered experimental.</li>
<li>Python scripts that use external libraries can run <a href="https://github.com/Azure/azure-functions-host/issues/1626">exceedingly slow</a>.</li>
<li>Getting the environment set up is a bit of a hassle.</li>
</ol>
<p>On the other hand, building a service proxy function with C# was so much easier, and it performed much better than a comparable Python function (~.5 seconds for C# compared to 5+ seconds for Python).</p>
<p>Here are the steps I took to build my C# service proxy function:</p>
<ol>
<li>Create a C# HTTP trigger function.</li>
<li>Create and upload a project.json file with a dependency on the RabbitMQ client (see below).</li>
<li>Take the &quot;RpcClient&quot; class from the <a href="https://www.rabbitmq.com/tutorials/tutorial-six-dotnet.html">RabbitMQ .Net RPC tutorial</a> and call it from within my function.</li>
</ol>
<p>Here's my project.json file:</p>
<pre><code>{
  &quot;frameworks&quot;: {
    &quot;net46&quot;:{
      &quot;dependencies&quot;: {
        &quot;RabbitMQ.Client&quot;: &quot;5.0.1&quot;
      }
    }
   }
}
</code></pre>
<p>And here's my run.csx file:</p>
<pre><code>using System.Net;
using System;
using System.Collections.Concurrent;
using System.Text;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;

public static async Task&lt;HttpResponseMessage&gt; Run(HttpRequestMessage req, TraceWriter log)
{
    log.Info(&quot;Processing request&quot;);

    // parse query parameter
    string query = req.GetQueryNameValuePairs()
        .FirstOrDefault(q =&gt; string.Compare(q.Key, &quot;query&quot;, true) == 0)
        .Value;

    // Get request body
    dynamic data = await req.Content.ReadAsAsync&lt;object&gt;();

    // Set name to query string or body data
    query = query ?? data?.query;

    var rpcClient = new RpcClient();
    
    log.Info(string.Format(&quot; [.] query start time {0}&quot;, DateTime.Now.ToString(&quot;MM/dd/yyyy hh:mm:ss.fff tt&quot;)));
    var response = rpcClient.Call(query);

    log.Info(string.Format(&quot; [.] query end time {0}&quot;, DateTime.Now.ToString(&quot;MM/dd/yyyy hh:mm:ss.fff tt&quot;)));
    rpcClient.Close();

    return req.CreateResponse(HttpStatusCode.OK, response);
}

public class RpcClient
{
    private readonly IConnection connection;
    private readonly IModel channel;
    private readonly string replyQueueName;
    private readonly EventingBasicConsumer consumer;
    private readonly BlockingCollection&lt;string&gt; respQueue = new BlockingCollection&lt;string&gt;();
    private readonly IBasicProperties props;

    public RpcClient()
    {
        var factory = new ConnectionFactory() { HostName = &quot;RABBITHOST&quot;, UserName=&quot;RABBITUSER&quot;, Password=&quot;RABBITUSERPASS&quot;  };

        connection = factory.CreateConnection();
        channel = connection.CreateModel();
        replyQueueName = channel.QueueDeclare().QueueName;
        consumer = new EventingBasicConsumer(channel);

        props = channel.CreateBasicProperties();
        var correlationId = Guid.NewGuid().ToString();
        props.CorrelationId = correlationId;
        props.ReplyTo = replyQueueName;

        consumer.Received += (model, ea) =&gt;
        {
            var body = ea.Body;
            var response = Encoding.UTF8.GetString(body);
            if (ea.BasicProperties.CorrelationId == correlationId)
            {
                respQueue.Add(response);
            }
        };
    }

    public string Call(string message)
    {
        var messageBytes = Encoding.UTF8.GetBytes(message);
        channel.BasicPublish(
            exchange: &quot;&quot;,
            routingKey: &quot;rpc_queue&quot;,
            basicProperties: props,
            body: messageBytes);

        channel.BasicConsume(
            consumer: consumer,
            queue: replyQueueName,
            autoAck: true);

        return respQueue.Take(); ;
    }

    public void Close()
    {
        connection.Close();
    }
}
</code></pre>
<p>Here's a screenshot showing me calling the C# function with Postman.<br>
<img src="https://alexanderdevelopment.net/content/images/2018/02/Postman_2018-02-05_22-02-52.png#img-thumbnail" alt="Building a simple service relay for Dynamics 365 CE with RabbitMQ and Python - part 4"></p>
<p>Because I did actually build a Python function, I will go ahead and share how I did it if you're interested. Here are the steps I took:</p>
<ol>
<li>Create a Python HTTP trigger function.</li>
<li>Install Python 3.6 via site extensions (see steps 2.1-2.4 <a href="https://stackoverflow.com/a/47213859">here</a>).</li>
<li>Install the necessary libraries using pip via <a href="https://david-obrien.net/2016/07/azure-functions-kudu/">KUDU</a>.</li>
</ol>
<p>Here's the Python function code:</p>
<pre><code>import os
import sys
import json
import pika
import uuid
import datetime

class CrmRpcClient(object):
    def __init__(self):
        #RabbitMQ connection details
        self.rabbituser = 'RABBITUSERNAME'
        self.rabbitpass = 'RABBITUSERPASS'
        self.rabbithost = 'RABBITHOST' 
        self.rabbitport = 5672
        self.rabbitqueue = 'rpc_queue'
        rabbitcredentials = pika.PlainCredentials(self.rabbituser, self.rabbitpass)
        rabbitparameters = pika.ConnectionParameters(host=self.rabbithost,
                                    port=self.rabbitport,
                                    virtual_host='/',
                                    credentials=rabbitcredentials)

        self.rabbitconn = pika.BlockingConnection(rabbitparameters)

        self.channel = self.rabbitconn.channel()

        #create an anonymous exclusive callback queue
        result = self.channel.queue_declare(exclusive=True)
        self.callback_queue = result.method.queue

        self.channel.basic_consume(self.on_response, no_ack=True,
                                   queue=self.callback_queue)

    #callback method for when a response is received - note the check for correlation id
    def on_response(self, ch, method, props, body):
        if self.corr_id == props.correlation_id:
            self.response = body

    #method to make the initial request
    def call(self, n):
        self.response = None
        #generate a new correlation id
        self.corr_id = str(uuid.uuid4())

        #publish the message to the rpc_queue - note the reply_to property is set to the callback queue from above
        self.channel.basic_publish(exchange='',
                                   routing_key=self.rabbitqueue,
                                   properties=pika.BasicProperties(
                                         reply_to = self.callback_queue,
                                         correlation_id = self.corr_id,
                                         ),
                                   body=n)
        while self.response is None:
            self.rabbitconn.process_data_events()
        return self.response

print(&quot; [.] query start time %r&quot; % str(datetime.datetime.now()))
#instantiate an rpc client
crm_rpc = CrmRpcClient()

postreqdata = json.loads(open(os.environ['req']).read())
query = postreqdata['query']

crm_rpc = CrmRpcClient()
print(&quot; [.] query start time %r&quot; % str(datetime.datetime.now()))
queryresponse = crm_rpc.call(query)
print(&quot; [.] query end time %r&quot; % str(datetime.datetime.now()))
response = open(os.environ['res'], 'w')
response.write(queryresponse.decode())
response.close()
</code></pre>
<p>Here's a screenshot showing me calling the Python function with Postman.<img src="https://alexanderdevelopment.net/content/images/2018/02/Postman_2018-02-05_22-10-20.png#img-thumbnail" alt="Building a simple service relay for Dynamics 365 CE with RabbitMQ and Python - part 4"></p>
<p>Note the difference in time between the two functions - 5.62 seconds for Python and .46 seconds for C#!</p>
<h4 id="securityandscalability">Security and scalability</h4>
<p>If you decide to use this approach in production, I'd suggest you carefully consider both security and scalability. Obviously the overall solution will only be as secure as your RabbitMQ broker and communications between the broker and its clients, so you'll want to look at best practices for access control and securing the communications with TLS. Here are some links for further reading on those subjects:</p>
<ul>
<li>TLS - <a href="https://www.rabbitmq.com/ssl.html">https://www.rabbitmq.com/ssl.html</a></li>
<li>Access control - <a href="https://www.rabbitmq.com/access-control.html">https://www.rabbitmq.com/access-control.html</a></li>
</ul>
<p>As for scalability, the approach I've shown creates a separate response queue for each consumer, but it can have problems scaling, especially if you are using a RabbitMQ cluster. You may want to look at the <a href="https://www.rabbitmq.com/direct-reply-to.html">&quot;direct reply-to&quot;</a> approach instead. For an interesting real-world overview of using direct reply-to, take a look at this <a href="https://facundoolano.wordpress.com/2016/06/26/real-world-rpc-with-rabbitmq-and-node-js/">blog post.</a>.</p>
<h4 id="wrappingup">Wrapping up</h4>
<p>I hope you've enjoyed this series and that it has given you some ideas about how to implement service relays in your Dynamics 365 CE projects. As I worked through the examples, I certainly learned a few new things, especially when I created my Python service proxy in Azure Functions.</p>
<p>Here are links to all the previous posts in this series.</p>
<ol>
<li><a href="https://alexanderdevelopment.net/post/2018/01/30/relaying-external-queries-to-on-premise-dynamics-365-ce-orgs-with-rabbitmq-and-python/">Part 1</a> - Series introduction</li>
<li><a href="https://alexanderdevelopment.net/post/2018/02/01/building-a-simple-service-relay-for-dynamics-365-ce-with-rabbitmq-and-python-part-2/">Part 2</a> - Solution prerequisites</li>
<li><a href="https://alexanderdevelopment.net/post/2018/02/05/building-a-simple-service-relay-for-dynamics-365-ce-with-rabbitmq-and-python-part-3/">Part 3</a> - Python code for the consumer and listener processes</li>
</ol>
<p>What do you think about this approach? Is it something you think you'd use in production? Let us know in the comments!</p>
</div>]]></content:encoded></item><item><title><![CDATA[Building a simple service relay for Dynamics 365 CE with RabbitMQ and Python - part 3]]></title><description><![CDATA[<div class="kg-card-markdown"><p>In my last <a href="https://alexanderdevelopment.net/post/2018/02/01/building-a-simple-service-relay-for-dynamics-365-ce-with-rabbitmq-and-python-part-2/">post</a> in this series, I walked through the prerequisites for building a simple service relay for Dynamics 365 CE with RabbitMQ and Python. In today's post I will show the Python code to make the service relay work.</p>
<p>As I described in the <a href="https://alexanderdevelopment.net/post/2018/01/30/relaying-external-queries-to-on-premise-dynamics-365-ce-orgs-with-rabbitmq-and-python/">first post</a> in this</p></div>]]></description><link>https://alexanderdevelopment.net/post/2018/02/05/building-a-simple-service-relay-for-dynamics-365-ce-with-rabbitmq-and-python-part-3/</link><guid isPermaLink="false">5a6cab4cc86c8900016cf352</guid><category><![CDATA[Microsoft Dynamics CRM]]></category><category><![CDATA[Dynamics 365]]></category><category><![CDATA[integration]]></category><category><![CDATA[Python]]></category><category><![CDATA[RabbitMQ]]></category><dc:creator><![CDATA[Lucas Alexander]]></dc:creator><pubDate>Mon, 05 Feb 2018 17:57:29 GMT</pubDate><media:content url="https://alexanderdevelopment.net/content/images/2018/02/simple-service-relay-1.png" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><img src="https://alexanderdevelopment.net/content/images/2018/02/simple-service-relay-1.png" alt="Building a simple service relay for Dynamics 365 CE with RabbitMQ and Python - part 3"><p>In my last <a href="https://alexanderdevelopment.net/post/2018/02/01/building-a-simple-service-relay-for-dynamics-365-ce-with-rabbitmq-and-python-part-2/">post</a> in this series, I walked through the prerequisites for building a simple service relay for Dynamics 365 CE with RabbitMQ and Python. In today's post I will show the Python code to make the service relay work.</p>
<p>As I described in the <a href="https://alexanderdevelopment.net/post/2018/01/30/relaying-external-queries-to-on-premise-dynamics-365-ce-orgs-with-rabbitmq-and-python/">first post</a> in this series, this approach relies on a consumer process and a queue listener process that can both access a RabbitMQ message broker.</p>
<blockquote>
<p>A consumer writes a request to a cloud-hosted RabbitMQ request queue (either directly or through a proxy service) and starts waiting for a response. On the other end, a Python script monitors the request queue for inbound requests. When it sees a new one, it executes the appropriate request through the Dynamics 365 Web API and writes the response back to a client-specific RabbitMQ response queue. The consumer then picks up the response from the queue.</p>
</blockquote>
<p>This solution is based on the remote procedure call (RPC) approach shown <a href="https://www.rabbitmq.com/tutorials/tutorial-six-python.html">here</a>. The main difference is that I have added logic to the queue monitoring script to query the Dynamics 365 Web API based on the inbound request from the consumer.</p>
<h4 id="consumersample">Consumer sample</h4>
<p>The consumer does the following:</p>
<ol>
<li>Read the text of the request to write to the queue from a command-line argument.</li>
<li>Establish a connection to the RabbitMQ broker.</li>
<li>Create a new anonymous, exclusive callback queue.</li>
<li>Write a request message a queue called &quot;rpc_queue.&quot; This message will include the callback queue as its &quot;reply_to&quot; property.</li>
<li>Monitor the callback queue for a response.</li>
</ol>
<p>There's no validation in this sample, so if you run it without a command-line argument, it will just throw an error and exit.</p>
<pre><code>import sys
import pika
import uuid
import datetime

class CrmRpcClient(object):
    def __init__(self):
        #RabbitMQ connection details
        self.rabbituser = 'crmuser'
        self.rabbitpass = 'crmpass'
        self.rabbithost = '127.0.0.1' 
        self.rabbitport = 5672
        self.rabbitqueue = 'rpc_queue'
        rabbitcredentials = pika.PlainCredentials(self.rabbituser, self.rabbitpass)
        rabbitparameters = pika.ConnectionParameters(host=self.rabbithost,
                                    port=self.rabbitport,
                                    virtual_host='/',
                                    credentials=rabbitcredentials)

                self.rabbitconn = pika.BlockingConnection(rabbitparameters)

        self.channel = self.rabbitconn.channel()

        #create an anonymous exclusive callback queue
        result = self.channel.queue_declare(exclusive=True)
        self.callback_queue = result.method.queue

        self.channel.basic_consume(self.on_response, no_ack=True,
                                   queue=self.callback_queue)

    #callback method for when a response is received - note the check for correlation id
    def on_response(self, ch, method, props, body):
        if self.corr_id == props.correlation_id:
            self.response = body

    #method to make the initial request
    def call(self, n):
        self.response = None
        #generate a new correlation id
        self.corr_id = str(uuid.uuid4())

        #publish the message to the rpc_queue - note the reply_to property is set to the callback queue from above
        self.channel.basic_publish(exchange='',
                                   routing_key=self.rabbitqueue,
                                   properties=pika.BasicProperties(
                                         reply_to = self.callback_queue,
                                         correlation_id = self.corr_id,
                                         ),
                                   body=n)
        while self.response is None:
            self.rabbitconn.process_data_events()
        return self.response

#instantiate an rpc client
crm_rpc = CrmRpcClient()

#read the request from the command line
request = sys.argv[1]

#make the request and get the response
print(&quot; [x] Requesting crm data(&quot;+request+&quot;)&quot;)
print(&quot; [.] Start time %s&quot; % str(datetime.datetime.now()))
response = crm_rpc.call(request)

#convert the response message body from the queue to a string 
decoderesponse = response.decode()

#print the output
print(&quot; [.] Received response: %s&quot; % decoderesponse)
print(&quot; [.] End time %s&quot; % str(datetime.datetime.now()))
</code></pre>
<h4 id="queuelistenersample">Queue listener sample</h4>
<p>The queue listener does the following:</p>
<ol>
<li>Establish a connection to the RabbitMQ broker</li>
<li>Monitor &quot;rpc_queue&quot; queue.</li>
<li>When a new message from the &quot;rpc_queue&quot; queue is delivered, decode the message body as a string, and determine what Web API query to execute. Note: This sample can return a list of contacts or accounts from Dynamics 365 CE based on the request the consumer sends (&quot;getcontacts&quot; or &quot;getaccounts&quot;). If any other request is received, the listener will return an error message to the consumer callback queue.</li>
<li>Execute the appropriate query against the Dynamics 365 Web API and write the response to the callback queue the client established originally.</li>
</ol>
<pre><code>import pika
import requests
from requests_ntlm import HttpNtlmAuth
import json

#NTLM credentials to access on-prem Dynamics 365 Web API
username = 'DOMAIN\\USERNAME'
userpassword = 'PASSWORD'

#full path to Web API
crmwebapi = 'http://33.0.0.16/lucastest02/api/data/v8.1'

#RabbitMQ connection details
rabbituser = 'crmuser'
rabbitpass = 'crmpass'
rabbithost = '127.0.0.1' 
rabbitport = 5672

#method to execute a Web API query based on the client request
def processquery(query):
    #set the Web API request headers
    crmrequestheaders = {
        'OData-MaxVersion': '4.0',
        'OData-Version': '4.0',
        'Accept': 'application/json',
        'Content-Type': 'application/json; charset=utf-8',
        'Prefer': 'odata.maxpagesize=500',
        'Prefer': 'odata.include-annotations=OData.Community.Display.V1.FormattedValue'
    }

    #determine which Web API query to execute
    if query == 'getcontacts':
        crmwebapiquery = '/contacts?$select=fullname,contactid'
    elif query == 'getaccounts':
        crmwebapiquery = '/accounts?$select=name,accountid'
    else:
        #only handle 'getcontacts' or 'getaccounts' requests
        return 'Operation not supported'

    #execute the query
    crmres = requests.get(crmwebapi+crmwebapiquery, headers=crmrequestheaders,auth=HttpNtlmAuth(username,userpassword))
    
    #get the results json
    crmjson = crmres.json()

    #return the json
    return crmjson

#method to handle new inbound requests
def on_request(ch, method, props, body):
    #convert the message body from the queue to a string
    decodebody = body.decode('utf-8')

    #print the request
    print(&quot; [.] Received request: '%s'&quot; % decodebody)

    #process the request query
    response = processquery(decodebody)

    #publish the response back to 'reply-to' queue from the request message and set the correlation id
    ch.basic_publish(exchange='',
                     routing_key=props.reply_to,
                     properties=pika.BasicProperties(correlation_id = \
                                                         props.correlation_id),
                     body=str(response).encode(encoding=&quot;utf-8&quot;, errors=&quot;strict&quot;))
    ch.basic_ack(delivery_tag = method.delivery_tag)

print(&quot; [x] Awaiting RPC requests&quot;)

#connect to RabbitMQ broker
rabbitcredentials = pika.PlainCredentials(rabbituser, rabbitpass)
rabbitparameters = pika.ConnectionParameters(host=rabbithost,
                               port=rabbitport,
                               virtual_host='/',
                               credentials=rabbitcredentials)
rabbitconn = pika.BlockingConnection(rabbitparameters)
channel = rabbitconn.channel()

#declare the 'rpc_queue' queue
channel.queue_declare(queue='rpc_queue')

#set qos settings for the channel
channel.basic_qos(prefetch_count=1)

#assign the 'on_request' method as a callback for when new messages delivered from the 'rpc_queue' queue
channel.basic_consume(on_request, queue='rpc_queue')

#start listening for requests
channel.start_consuming()
</code></pre>
<h4 id="tryingitout">Trying it out</h4>
<p>As I mentioned in my last post, I initially wrote my code to use a RabbitMQ broker running on my local PC, so that's why the connections in the samples show 127.0.0.1 as the host. For a demo, I've spun up a copy of RabbitMQ in a Docker container in the cloud and updated my connection parameters accordingly, but I am still running my queue listener and consumer processes on my local PC.</p>
<p>When the listener first starts, it displays a simple status message.<br>
<img src="https://alexanderdevelopment.net/content/images/2018/02/1_start_listener.png#img-thumbnail" alt="Building a simple service relay for Dynamics 365 CE with RabbitMQ and Python - part 3"></p>
<p>Then I execute a &quot;getcontacts&quot; request from the consumer in a separate window.<br>
<img src="https://alexanderdevelopment.net/content/images/2018/02/2_get_contacts.png#img-thumbnail" alt="Building a simple service relay for Dynamics 365 CE with RabbitMQ and Python - part 3"></p>
<p>From the timestamps before and after the request, you can see the round-trip time is less than .2 seconds, which includes two round trips between my local PC and the cloud-based RabbitMQ broker <em>plus</em> the actual query processing time in my local Dynamics 365 CE org.</p>
<p>Then I execute a &quot;getaccounts&quot; request.<br>
<img src="https://alexanderdevelopment.net/content/images/2018/02/4_get_accounts.png#img-thumbnail" alt="Building a simple service relay for Dynamics 365 CE with RabbitMQ and Python - part 3"></p>
<p>This request was also fulfilled in less than .2 seconds.</p>
<p>Finally I execute an invalid request to show what the error response looks like.<br>
<img src="https://alexanderdevelopment.net/content/images/2018/02/6_get_leads.png#img-thumbnail" alt="Building a simple service relay for Dynamics 365 CE with RabbitMQ and Python - part 3"></p>
<p>You'll note the total time from request to response is only about .05 seconds less than the total time for the valid queries. That indicates most of the time used in these samples is being spent on the round trips between my local PC and the RabbitMQ broker, which is not surprising.</p>
<p>Meanwhile, the queue listener wrote a simple status update for every request it received. If I were using this in production, I would use more sophisticated logging.<br>
<img src="https://alexanderdevelopment.net/content/images/2018/02/7_listener_output.png#img-thumbnail" alt="Building a simple service relay for Dynamics 365 CE with RabbitMQ and Python - part 3"></p>
<h4 id="wrappingup">Wrapping up</h4>
<p>That's it for now. In my next (and final) post in this series, I will show how you can use <a href="https://azure.microsoft.com/en-us/services/functions/">Azure Functions</a> to make a consumer service proxy so consuming applications don't have to access to your RabbitMQ broker directly, and I will also discuss some general thoughts on security and scalability for the service .</p>
</div>]]></content:encoded></item><item><title><![CDATA[Building a simple service relay for Dynamics 365 CE with RabbitMQ and Python - part 2]]></title><description><![CDATA[<div class="kg-card-markdown"><p>In my <a href="https://alexanderdevelopment.net/post/2018/01/30/relaying-external-queries-to-on-premise-dynamics-365-ce-orgs-with-rabbitmq-and-python/">last post</a> in this series, I outlined an approach for building a simple service relay with <a href="https://www.rabbitmq.com/">RabbitMQ</a> and <a href="https://www.python.org/">Python</a> to easily expose an on-premises Dynamics 365 Customer Engagement organization to external consumers. In this post I will walk through the prerequisites for building this out. I'm assuming you</p></div>]]></description><link>https://alexanderdevelopment.net/post/2018/02/01/building-a-simple-service-relay-for-dynamics-365-ce-with-rabbitmq-and-python-part-2/</link><guid isPermaLink="false">5a6ca8fec86c8900016cf351</guid><category><![CDATA[Microsoft Dynamics CRM]]></category><category><![CDATA[Dynamics 365]]></category><category><![CDATA[integration]]></category><category><![CDATA[Python]]></category><category><![CDATA[RabbitMQ]]></category><dc:creator><![CDATA[Lucas Alexander]]></dc:creator><pubDate>Fri, 02 Feb 2018 03:24:51 GMT</pubDate><media:content url="https://alexanderdevelopment.net/content/images/2018/02/simple-service-relay.png" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><img src="https://alexanderdevelopment.net/content/images/2018/02/simple-service-relay.png" alt="Building a simple service relay for Dynamics 365 CE with RabbitMQ and Python - part 2"><p>In my <a href="https://alexanderdevelopment.net/post/2018/01/30/relaying-external-queries-to-on-premise-dynamics-365-ce-orgs-with-rabbitmq-and-python/">last post</a> in this series, I outlined an approach for building a simple service relay with <a href="https://www.rabbitmq.com/">RabbitMQ</a> and <a href="https://www.python.org/">Python</a> to easily expose an on-premises Dynamics 365 Customer Engagement organization to external consumers. In this post I will walk through the prerequisites for building this out. I'm assuming you have access to a Dynamics 365 CE organization, so I'm going to skip the setup for that and focus on just RabbitMQ and Python today.</p>
<h4 id="settinguprabbitmq">Setting up RabbitMQ</h4>
<p>Back in 2015 When I first blogged about RabbitMQ and Dynamics 365, I wrote a <a href="https://alexanderdevelopment.net/post/2015/01/14/using-rabbitmq-as-a-message-broker-in-dynamics-crm-data-interfaces-part-2/">detailed post</a> showing how to install and configure RabbitMQ. Since then I have discovered the joys of <a href="https://www.docker.com/">Docker</a>, which makes the process significantly easier. If you have access to Docker, I highly recommend using it. Once you have Docker running, you can use one of the <a href="https://hub.docker.com/_/rabbitmq/">official RabbitMQ images</a>. For this project, I initially used the rabbitmq:3-management image in <a href="https://www.docker.com/docker-windows">Docker for Windows</a> running on my local PC. After I got the basic functionality working, I then moved to an instance of Docker running in the cloud on a <a href="https://www.digitalocean.com" target="_blank">Digital Ocean</a> VPS.</p>
<p>If don't want to use Docker, you can use a full RabbitMQ install like I showed <a href="https://alexanderdevelopment.net/post/2015/01/14/using-rabbitmq-as-a-message-broker-in-dynamics-crm-data-interfaces-part-2/">previously</a>. The main thing to remember is that no matter how you set up your RabbitMQ server, if it is not accessible from the public internet, you will not be able to use it as a service relay between an on-premises Dynamics 365 org and external consumers.</p>
<h4 id="settinguppython">Setting up Python</h4>
<p>I'm assuming if you've gotten this far, you have a functional Python development environment (if not, give <a href="https://code.visualstudio.com/docs/languages/python">Visual Studio Code</a> a try), and the code I have written works in Python versions 2.7 or 3.x. In order to connect to both RabbitMQ and Dynamics 365, you will need a few additional packages. To connect to RabbitMQ, <a href="https://pika.readthedocs.io/en/0.11.2/">Pika</a> is the RabbitMQ team's recommended Python client, and you can get it using <a href="https://pypi.python.org/pypi/pip">pip</a>.</p>
<p>To communicate with Dynamics 365, you'll need to use the Web API, but authentication will be handled differently depending on whether you connect to an on-premises org or an online / IFD org. For online or IFD orgs, you can either use <a href="https://jlattimer.blogspot.com/2015/11/crm-web-api-using-python.html">ADAL</a> or this <a href="http://alexanderdevelopment.net/post/2016/11/27/dynamics-365-and-python-integration-using-the-web-api/">alternate approach</a> I described back in 2016. If you have an on-premises org, you can authenticate using the requests_ntlm package like I showed <a href="https://alexanderdevelopment.net/post/2018/01/15/connecting-to-an-on-premise-dynamics-365-org-from-python/">here</a>. As with the Pika client, all the packages you need to connect to Dynamics 365 are also available via pip.</p>
<h4 id="wrappingup">Wrapping up</h4>
<p>That's it for today. In my next post in this series I will show the Python code you need to make this service relay work.</p>
</div>]]></content:encoded></item><item><title><![CDATA[Building a simple service relay for Dynamics 365 CE with RabbitMQ and Python - part 1]]></title><description><![CDATA[<div class="kg-card-markdown"><p>Integrating with external systems is a common requirement in Dynamics 365 Customer Engagement projects, but when the project involves an on-premises instance of Dynamics 365, routing requests from external systems through your firewall can present an additional challenge. Over the course of the next few posts, I will show you</p></div>]]></description><link>https://alexanderdevelopment.net/post/2018/01/30/relaying-external-queries-to-on-premise-dynamics-365-ce-orgs-with-rabbitmq-and-python/</link><guid isPermaLink="false">5a636975e2df920001a88f8e</guid><category><![CDATA[Microsoft Dynamics CRM]]></category><category><![CDATA[Dynamics 365]]></category><category><![CDATA[integration]]></category><category><![CDATA[Python]]></category><category><![CDATA[RabbitMQ]]></category><dc:creator><![CDATA[Lucas Alexander]]></dc:creator><pubDate>Wed, 31 Jan 2018 01:01:10 GMT</pubDate><media:content url="https://alexanderdevelopment.net/content/images/2018/01/simple-service-relay-1.png" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><img src="https://alexanderdevelopment.net/content/images/2018/01/simple-service-relay-1.png" alt="Building a simple service relay for Dynamics 365 CE with RabbitMQ and Python - part 1"><p>Integrating with external systems is a common requirement in Dynamics 365 Customer Engagement projects, but when the project involves an on-premises instance of Dynamics 365, routing requests from external systems through your firewall can present an additional challenge. Over the course of the next few posts, I will show you can easily build a simple service relay with <a href="https://www.rabbitmq.com/">RabbitMQ</a> and <a href="https://www.python.org/">Python</a> to handle inbound requests from external data interface consumers.</p>
<p>Here's how my approach works. A consumer writes a request to a cloud-hosted RabbitMQ request queue (either directly or through a proxy service) and starts waiting for a response. On the other end, a Python script monitors the request queue for inbound requests. When it sees a new one, it executes the appropriate request through the Dynamics 365 Web API and writes the response back to a client-specific RabbitMQ response queue. The consumer then picks up the response from the queue. This way the consumer doesn't need to know anything other than how to write the initial request, and no extra inbound firewall ports need to be opened.</p>
<p>This diagram shows an overview of the process. <img src="https://alexanderdevelopment.net/content/images/2018/01/simple-service-relay.png#img-thumbnail" alt="Building a simple service relay for Dynamics 365 CE with RabbitMQ and Python - part 1"></p>
<p>Although my original goal was to accelerate the deployment of data interfaces for on-premises Dynamics 365 CE instances, a simple service relay like this could also be useful for IFD or Dynamics 365 online deployments if you don't want to allow direct access to your organization. Because the queue monitoring process is single-threaded, it's an easy way to throttle requests, but you can run multiple instances of the queue monitor script if you want to increase the number of concurrent requests the relay can process.</p>
<h4 id="whyusethisapproach">Why use this approach?</h4>
<p>There are lots message brokers and service bus offerings (Azure Service Bus, IBM MQ, Amazon SQS, etc.) you could use to build a service relay. In fact there's even an Azure offering called <a href="https://docs.microsoft.com/en-us/azure/service-bus-relay/relay-what-is-it">Azure Relay</a> that aims to solve exactly the same problem that my approach does, but not just for Dynamics 365, so &quot;why use this?&quot; is a great question.</p>
<p>First, I think RabbitMQ is just a great tool, and I previously wrote a <a href="https://alexanderdevelopment.net/post/2015/01/27/using-rabbitmq-as-a-message-broker-in-dynamics-crm-data-interfaces-part-5/">five-part series</a> about using RabbitMQ with Dynamics 365 (back when it was still called Dynamics CRM). Second, using RabbitMQ instead of a cloud-specific service bus offering gives you maximum flexibility in where you host your request and response queues and how you chose to scale. For example, my RabbitMQ broker runs in a <a href="https://www.docker.com">Docker</a> container on a <a href="https://www.digitalocean.com/" target="_blank">Digital Ocean</a> VPS. If I ever decide to move off of Digital Ocean, I can easily switch to any IaaS or VPS provider. I can also configure a RabbitMQ cluster to achieve significantly faster throughput.</p>
<p>As for why I'm using Python instead of C#, which is probably more familiar to most Dynamics 365 developers, Python also makes this approach more flexible. Using Python means I'm not tied to the Dynamics 365 SDK client libraries or a Windows host for running my queue monitoring process, and I can easily package my monitoring process in a Docker image. <em>(Although I highly recommend Python, there are RabbitMQ clients for <a href="https://www.nuget.org/packages/RabbitMQ.Client">.Net</a>, and you can also find RabbitMQ tutorials for other languages including Java, Ruby and JavaScript <a href="https://www.rabbitmq.com/getstarted.html">here</a>.)</em></p>
<h4 id="wrappingup">Wrapping up</h4>
<p>That's it for now. In my next post in this series I will walk through the prerequisites for building the simple service relay.</p>
<p>How have you handled inbound data interfaces for on-premises Dynamics 365 CE organizations? Let us know in the comments!</p>
</div>]]></content:encoded></item><item><title><![CDATA[Accessing an on-premises Dynamics 365 organization from Python]]></title><description><![CDATA[<div class="kg-card-markdown"><p>I've previously <a href="https://alexanderdevelopment.net/post/2016/11/27/dynamics-365-and-python-integration-using-the-web-api/">showed</a> how to access online and IFD instances of Dynamics 365 Customer Engagement from Python code. Because that sample code authenticated to the Web API using OAuth, it won't work with on-premises instances. Recently I've been doing some work with Python and an on-premises Dynamics 365 organization, so</p></div>]]></description><link>https://alexanderdevelopment.net/post/2018/01/15/connecting-to-an-on-premise-dynamics-365-org-from-python/</link><guid isPermaLink="false">5a5939bae2df920001a88f77</guid><category><![CDATA[Microsoft Dynamics CRM]]></category><category><![CDATA[Dynamics 365]]></category><category><![CDATA[Python]]></category><category><![CDATA[programming]]></category><category><![CDATA[integration]]></category><dc:creator><![CDATA[Lucas Alexander]]></dc:creator><pubDate>Mon, 15 Jan 2018 14:58:00 GMT</pubDate><media:content url="https://alexanderdevelopment.net/content/images/2018/01/Code_2018-01-12_17-00-50.png" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><img src="https://alexanderdevelopment.net/content/images/2018/01/Code_2018-01-12_17-00-50.png" alt="Accessing an on-premises Dynamics 365 organization from Python"><p>I've previously <a href="https://alexanderdevelopment.net/post/2016/11/27/dynamics-365-and-python-integration-using-the-web-api/">showed</a> how to access online and IFD instances of Dynamics 365 Customer Engagement from Python code. Because that sample code authenticated to the Web API using OAuth, it won't work with on-premises instances. Recently I've been doing some work with Python and an on-premises Dynamics 365 organization, so I thought I'd share a sample that shows how to authenticate to the Web API using NTLM.</p>
<pre><code>import requests
from requests_ntlm import HttpNtlmAuth
import json

username = 'companyx\\administrator'
userpassword = 'PASSWORD GOES HERE'

#set these values to query your crm data
crmwebapi = 'http://33.0.0.16/lucastest02/api/data/v8.1'
crmwebapiquery = '/contacts?$select=fullname,contactid'

crmrequestheaders = {
    'OData-MaxVersion': '4.0',
    'OData-Version': '4.0',
    'Accept': 'application/json',
    'Content-Type': 'application/json; charset=utf-8',
    'Prefer': 'odata.maxpagesize=500',
    'Prefer': 'odata.include-annotations=OData.Community.Display.V1.FormattedValue'
}

print('making crm request . . .')
crmres = requests.get(crmwebapi+crmwebapiquery, headers=crmrequestheaders,auth=HttpNtlmAuth(username,userpassword))
print('crm response received . . .')
try:
    print('parsing crm response . . .')
    crmresults = crmres.json()
    for x in crmresults['value']:
        print (x['fullname'] + ' - ' + x['contactid'])
except KeyError:
    print('Could not parse CRM results')
</code></pre>
<p>As you can see, this code doesn't retrieve an OAuth token before calling the Dynamics 365 Web API, but rather it uses the <a href="https://github.com/requests/requests-ntlm">requests-ntlm</a> package to authenticate directly to the Web API using a username and password. Other than that small change, everything else works the same as in my previous examples.</p>
</div>]]></content:encoded></item><item><title><![CDATA[Dynamics 365 and Python integration using the Web API - part 2]]></title><description><![CDATA[<div class="kg-card-markdown"><p>Last week I wrote a <a href="https://alexanderdevelopment.net/post/2017/02/16/dynamics-365-and-node-js-integration-using-the-web-api-part-2/">post</a> that showed how to update Dynamics 365 data from a Node.js application using the Web API. Today I will share equivalent Python code. This code builds on my <a href="https://alexanderdevelopment.net/post/2016/11/27/dynamics-365-and-python-integration-using-the-web-api/">Dynamics 365 and Python integration using the Web API</a> post from last year, so if</p></div>]]></description><link>https://alexanderdevelopment.net/post/2017/02/19/dynamics-365-and-python-integration-using-the-web-api-part-2/</link><guid isPermaLink="false">5a5837246636a30001b9788f</guid><category><![CDATA[Microsoft Dynamics CRM]]></category><category><![CDATA[Dynamics 365]]></category><category><![CDATA[Python]]></category><category><![CDATA[integration]]></category><category><![CDATA[programming]]></category><dc:creator><![CDATA[Lucas Alexander]]></dc:creator><pubDate>Sun, 19 Feb 2017 21:15:02 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>Last week I wrote a <a href="https://alexanderdevelopment.net/post/2017/02/16/dynamics-365-and-node-js-integration-using-the-web-api-part-2/">post</a> that showed how to update Dynamics 365 data from a Node.js application using the Web API. Today I will share equivalent Python code. This code builds on my <a href="https://alexanderdevelopment.net/post/2016/11/27/dynamics-365-and-python-integration-using-the-web-api/">Dynamics 365 and Python integration using the Web API</a> post from last year, so if you haven't read that yet, please take a look before you proceed.</p>
<h4 id="updatingasingleproperty">Updating a single property</h4>
<p>To update a single property on a record in Dynamics 365, you can make a PUT request to the Web API. The raw HTTP request to update the first name for a contact would look like this:</p>
<pre><code>PUT [Organization URI]/api/data/v8.2/contacts(00000000-0000-0000-0000-000000000001)/firstname HTTP/1.1
Content-Type: application/json
OData-MaxVersion: 4.0
OData-Version: 4.0

{&quot;value&quot;: &quot;Demo-Firstname&quot;}
</code></pre>
<p>Assuming you have retrieved the OAuth token for authenticating to Dynamics 365 as I outlined in my earlier Python-Web API blog post, here is a sample Python function to make a PUT update request:</p>
<pre><code>def updateContactPut(accesstoken, contactid):
    crmrequestheaders = {
        'Authorization': 'Bearer ' + accesstoken,
        'Content-Type': 'application/json',
        'OData-MaxVersion': '4.0',
        'OData-Version': '4.0'
    }
 
    #make the crm request
    contactObj={
        'value':'Firstname PUT test'	
    };
    crmres = requests.put(crmwebapi+'/contacts('+contactid+')/firstname', headers=crmrequestheaders, data=json.dumps(contactObj))
    print(crmres)
</code></pre>
<h4 id="updatingmultipleproperties">Updating multiple properties</h4>
<p>To update a multiple properties on a record in Dynamics 365, you must make a PATCH request to the Web API. The raw HTTP request to update the first name and last name for a contact would look like this:</p>
<pre><code>PUT [Organization URI]/api/data/v8.2/contacts(00000000-0000-0000-0000-000000000001) HTTP/1.1
Content-Type: application/json
OData-MaxVersion: 4.0
OData-Version: 4.0

{
&quot;firstname&quot;: &quot;Demo-Firstname&quot;,
&quot;lastname&quot;: &quot;Demo-Lastname&quot;
}
</code></pre>
<p>Again, assuming you have retrieved the OAuth token for authenticating to Dynamics 365 as I outlined in my earlier blog post, here is a sample Python function to make a PATCH update request:</p>
<pre><code>def updateContactPatch(accesstoken, contactid):
    crmrequestheaders = {
        'Authorization': 'Bearer ' + accesstoken,
        'Content-Type': 'application/json',
        'OData-MaxVersion': '4.0',
        'OData-Version': '4.0'
    }
 
    #make the crm request
    contactObj={
        'firstname':'Firstname test',
        'lastname':'Lastname test'	
    };
    crmres = requests.patch(crmwebapi+'/contacts('+contactid+')', headers=crmrequestheaders, data=json.dumps(contactObj))
    print(crmres)
</code></pre>
<h4 id="furtherreading">Further reading</h4>
<p>For more information on updating and deleting data with the Dynamics 365 Web API, take a look at the <a href="https://msdn.microsoft.com/en-us/library/mt607664.aspx">&quot;Update and delete entities using the Web API&quot;</a> article on MSDN.</p>
</div>]]></content:encoded></item><item><title><![CDATA[Scheduling Dynamics 365 workflows with Azure Functions and Python]]></title><description><![CDATA[<div class="kg-card-markdown"><p>Last week I shared a <a href="https://alexanderdevelopment.net/post/2016/11/25/scheduling-dynamics-365-workflows-with-azure-functions/">solution</a> for Scheduling Dynamics 365 workflows with Azure Functions and Node.js. In this post, I will show how to achieve equivalent functionality using Python. The actual Python code is simpler than my Node.js example, but the Azure Functions configuration is much more complicated.</p></div>]]></description><link>https://alexanderdevelopment.net/post/2016/11/29/scheduling-dynamics-365-workflows-with-azure-functions-and-python/</link><guid isPermaLink="false">5a5837246636a30001b97872</guid><category><![CDATA[Microsoft Dynamics CRM]]></category><category><![CDATA[Dynamics 365]]></category><category><![CDATA[Python]]></category><category><![CDATA[Azure]]></category><category><![CDATA[demonstrations]]></category><dc:creator><![CDATA[Lucas Alexander]]></dc:creator><pubDate>Tue, 29 Nov 2016 12:00:00 GMT</pubDate><media:content url="https://alexanderdevelopment.net/content/images/2016/11/06-2.png" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><img src="https://alexanderdevelopment.net/content/images/2016/11/06-2.png" alt="Scheduling Dynamics 365 workflows with Azure Functions and Python"><p>Last week I shared a <a href="https://alexanderdevelopment.net/post/2016/11/25/scheduling-dynamics-365-workflows-with-azure-functions/">solution</a> for Scheduling Dynamics 365 workflows with Azure Functions and Node.js. In this post, I will show how to achieve equivalent functionality using Python. The actual Python code is simpler than my Node.js example, but the Azure Functions configuration is much more complicated.</p>
<p>First, here's the Python script I am using. It does this following:</p>
<ol>
<li>Request an OAuth token using a username and password.</li>
<li>Query the Dynamics 365 Web API for accounts with names that start with the letter &quot;F.&quot;</li>
<li>Execute a workflow for each record that was retrieved in the previous step. The workflow that I am executing is the same workflow I used in my Node.js example to create a note on an account.</li>
</ol>
<pre><code>import os
import sys
cwd = os.getcwd()
sitepackage = cwd + &quot;\site-packages&quot;
sys.path.append(sitepackage)

import requests
import json

#set these values to retrieve the oauth token
crmorg = 'https://CRMORG.crm.dynamics.com' #base url for crm org  
clientid = '00000000-0000-0000-0000-000000000000' #application client id  
username = 'xxxxxx@xxxxxxxx' #username  
userpassword = 'xxxxxxxx' #password  
tokenendpoint = 'https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000/oauth2/token' #oauth token endpoint

#set these values to query your crm data
crmwebapi = 'https://lucasdemo01.api.crm.dynamics.com/api/data/v8.2'
crmwebapiquery = &quot;/accounts?$select=name,accountid&amp;$filter=startswith(name,'f')&quot;

workflowid = 'DC8519EC-F3CE-4BC9-BB79-DF2AD70217A1'; #guid for the workflow you want to execute

def start():
    #build the authorization request
    tokenpost = {
        'client_id':clientid,
        'resource':crmorg,
        'username':username,
        'password':userpassword,
        'grant_type':'password'
    }

    #make the token request
    print('requesting token . . .')
    tokenres = requests.post(tokenendpoint, data=tokenpost)
    print('token response received. . .')

    accesstoken = ''

    #extract the access token
    try:
        print('parsing token response . . .')
        accesstoken = tokenres.json()['access_token']
        #print('accesstoken is - ' + accesstoken)

    except(KeyError):
        print('Could not get access token')

    if(accesstoken!=''):
        crmrequestheaders = {
            'Authorization': 'Bearer ' + accesstoken,
            'OData-MaxVersion': '4.0',
            'OData-Version': '4.0',
            'Accept': 'application/json',
            'Content-Type': 'application/json; charset=utf-8',
            'Prefer': 'odata.maxpagesize=500',
            'Prefer': 'odata.include-annotations=OData.Community.Display.V1.FormattedValue'
        }

        print('making crm account request . . .')
        crmres = requests.get(crmwebapi+crmwebapiquery, headers=crmrequestheaders)
        print('crm account response received . . .')
        try:
            print('parsing crm account response . . .')
            crmresults = crmres.json()
            for x in crmresults['value']:
                print (x['name'] + ' - ' + x['accountid'])
                runWorkflow(accesstoken, x['accountid'])
        except KeyError:
            print('Could not parse CRM account results')

        
def runWorkflow(token, entityid):
    crmwebapiworkflowpath = &quot;/workflows(&quot;+workflowid+&quot;)/Microsoft.Dynamics.CRM.ExecuteWorkflow&quot;

    #set the web api request headers
    requestheaders = { 
        'Authorization': 'Bearer ' + token,
        'OData-MaxVersion': '4.0',
        'OData-Version': '4.0',
        'Accept': 'application/json',
        'Content-Type': 'application/json; charset=utf-8'
    };
    
    reqobj = {'EntityId': entityid}
    
    print('  calling workflow for ' + entityid)
	
    crmres = requests.post(crmwebapi+crmwebapiworkflowpath, headers=requestheaders, data=json.dumps(reqobj))
    if(crmres.status_code == requests.codes.ok):
        print('    success ' + entityid)
    else:
        print('    error ' + entityid)

start()

</code></pre>
<p>To set it up as an Azure Function, after you have either created a new Function app or opened an existing Function app, do the following:</p>
<ol>
<li>
<p>Create a new function. Select &quot;create your own custom function&quot; at the bottom. <img src="https://alexanderdevelopment.net/content/images/2016/11/01-1.png#img-thumbnail" alt="Scheduling Dynamics 365 workflows with Azure Functions and Python"></p>
</li>
<li>
<p>Set language to &quot;Python&quot; and scenario to &quot;Experimental.&quot; Currently there is no timer trigger template for Python, but you can work around that by selecting the &quot;QueueTrigger-Python&quot; template in this step and manually changing the trigger later. <img src="https://alexanderdevelopment.net/content/images/2016/11/02-1.png#img-thumbnail" alt="Scheduling Dynamics 365 workflows with Azure Functions and Python"></p>
</li>
<li>
<p>Give your function a name and click create. Ignore the queue name and storage account connection values. <img src="https://alexanderdevelopment.net/content/images/2016/11/03-1.png#img-thumbnail" alt="Scheduling Dynamics 365 workflows with Azure Functions and Python"></p>
</li>
<li>
<p>Once the function is created, select the &quot;integrate&quot; tab from the menu on the left and delete the Azure Queue Storage trigger. <img src="https://alexanderdevelopment.net/content/images/2016/11/04-2.png#img-thumbnail" alt="Scheduling Dynamics 365 workflows with Azure Functions and Python"></p>
</li>
<li>
<p>Click &quot;new trigger&quot; at the top, and then select &quot;timer&quot; from the list that appears. <img src="https://alexanderdevelopment.net/content/images/2016/11/05-1.png#img-thumbnail" alt="Scheduling Dynamics 365 workflows with Azure Functions and Python"></p>
</li>
<li>
<p>Give it a name (or leave the default) and set the schedule. The <code>0 */5 * * * *</code> value here will execute every five minutes, just like in my previous Node.js example. <img src="https://alexanderdevelopment.net/content/images/2016/11/06-1.png#img-thumbnail" alt="Scheduling Dynamics 365 workflows with Azure Functions and Python"></p>
</li>
<li>
<p>After the function is configured to run on a timer trigger, you have to upload the Python Requests library that is used to make the web service calls. Open a new browser window to download the Requests library code from GitHub at <a href="https://github.com/kennethreitz/requests/releases/">https://github.com/kennethreitz/requests/releases/</a>. Select the most recent .zip file.</p>
</li>
<li>
<p>After it downloads, open it and extract the entire &quot;requests&quot; directory. It should contain a directory called &quot;packages.&quot;</p>
</li>
<li>
<p>Zip up just the &quot;requests&quot; directory in a separate &quot;requests.zip&quot; file.</p>
</li>
<li>
<p>Back in the Azure portal Function App blade, select &quot;function app settings&quot; from the menu on the left and then click &quot;Go to Kudu.&quot; <img src="https://alexanderdevelopment.net/content/images/2016/11/07-1.png#img-thumbnail" alt="Scheduling Dynamics 365 workflows with Azure Functions and Python"></p>
</li>
<li>
<p>In the window that opens, click the &quot;site&quot; link at the top. <img src="https://alexanderdevelopment.net/content/images/2016/11/08-1.png#img-thumbnail" alt="Scheduling Dynamics 365 workflows with Azure Functions and Python"></p>
</li>
<li>
<p>Then click the &quot;wwwroot&quot; link. <img src="https://alexanderdevelopment.net/content/images/2016/11/09-1.png#img-thumbnail" alt="Scheduling Dynamics 365 workflows with Azure Functions and Python"></p>
</li>
<li>
<p>Then click the name of your function. In this case it is &quot;CrmWorkflowTimerPython.&quot; <img src="https://alexanderdevelopment.net/content/images/2016/11/10-1.png#img-thumbnail" alt="Scheduling Dynamics 365 workflows with Azure Functions and Python"></p>
</li>
<li>
<p>Go to the cmd prompt at the bottom of the page and type <code>mkdir site-packages</code>. Click enter. You should see a new &quot;site-packages&quot; directory get created. <img src="https://alexanderdevelopment.net/content/images/2016/11/11-1.png#img-thumbnail" alt="Scheduling Dynamics 365 workflows with Azure Functions and Python"></p>
</li>
<li>
<p>Click on the new &quot;site-packages&quot; link to enter that directory.</p>
</li>
<li>
<p>Drag the &quot;requests&quot; .zip file you created in step 9 above into the &quot;site-packages&quot; directory like is shown. <img src="https://alexanderdevelopment.net/content/images/2016/11/12.png#img-thumbnail" alt="Scheduling Dynamics 365 workflows with Azure Functions and Python"></p>
</li>
<li>
<p>Once it is uploaded, extract it by typing <code>unzip requests.zip</code> at the cmd prompt and clicking enter. You should see something like this when it's complete. <img src="https://alexanderdevelopment.net/content/images/2016/11/13.png#img-thumbnail" alt="Scheduling Dynamics 365 workflows with Azure Functions and Python"></p>
</li>
<li>
<p>At this point, you can close the Kudu window.</p>
</li>
<li>
<p>From the Function App blade, select your Python function and click &quot;develop.&quot; Highlight everything and delete it. <img src="https://alexanderdevelopment.net/content/images/2016/11/14.png#img-thumbnail" alt="Scheduling Dynamics 365 workflows with Azure Functions and Python"></p>
</li>
<li>
<p>Paste the Python code from the beginning of this post. The highlighted lines in the image are how the script knows how to find the Requests library you uploaded earlier. Set any specifics relative to your Dynamics 365 organization, and click save. At this point it should just start running on the schedule you set earlier. <img src="https://alexanderdevelopment.net/content/images/2016/11/15-1.png#img-thumbnail" alt="Scheduling Dynamics 365 workflows with Azure Functions and Python"></p>
</li>
<li>
<p>You can see all the function invocation logs on the monitor tab. <img src="https://alexanderdevelopment.net/content/images/2016/11/chrome_2016-11-28_13-49-37.png#img-thumbnail" alt="Scheduling Dynamics 365 workflows with Azure Functions and Python"></p>
</li>
<li>
<p>Here are the notes that the workflow created in my Dynamics 365 online org. <img src="https://alexanderdevelopment.net/content/images/2016/11/chrome_2016-11-28_13-52-30.png#img-thumbnail" alt="Scheduling Dynamics 365 workflows with Azure Functions and Python"></p>
</li>
</ol>
<p>A few notes/caveats:</p>
<ol>
<li>My Python code has hardly any error handling right now. If the workflow execution call returns an error, the Python code will not recognize it as an error.</li>
<li>My CRM record retrieval is set to retrieve a maximum of 500 records. You would need to modify the Web API request logic to handle more.</li>
<li>Per the &quot;Best Practices for Azure Functions&quot; guide:</li>
</ol>
<blockquote>
<p>Assume your function could encounter an exception at any time. Design your functions with the ability to continue from a previous fail point during the next execution.</p>
</blockquote>
<p>This means you should put logic in your workflow to make sure that duplicate executions are avoided (unless that's what you intend to happen).</p>
<p>That's it for today. Until next time, happy coding!</p>
</div>]]></content:encoded></item><item><title><![CDATA[Dynamics 365 and Python integration using the Web API]]></title><description><![CDATA[<div class="kg-card-markdown"><p>A few days back I wrote a post that showed an easy way to set up <a href="https://alexanderdevelopment.net/post/2016/11/23/dynamics-365-and-node-js-integration-using-the-web-api/">Dynamics 365 and Node.js integration using the Web API</a>. Here is Python code that demonstrates equivalent functionality to query contacts and display their information:</p>
<pre><code>import requests
import json

#set these values to retrieve</code></pre></div>]]></description><link>https://alexanderdevelopment.net/post/2016/11/27/dynamics-365-and-python-integration-using-the-web-api/</link><guid isPermaLink="false">5a5837246636a30001b9786c</guid><category><![CDATA[Microsoft Dynamics CRM]]></category><category><![CDATA[Dynamics 365]]></category><category><![CDATA[programming]]></category><category><![CDATA[integration]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Lucas Alexander]]></dc:creator><pubDate>Sun, 27 Nov 2016 15:12:31 GMT</pubDate><media:content url="https://alexanderdevelopment.net/content/images/2016/11/pythonw_2016-11-27_09-10-32.png" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><img src="https://alexanderdevelopment.net/content/images/2016/11/pythonw_2016-11-27_09-10-32.png" alt="Dynamics 365 and Python integration using the Web API"><p>A few days back I wrote a post that showed an easy way to set up <a href="https://alexanderdevelopment.net/post/2016/11/23/dynamics-365-and-node-js-integration-using-the-web-api/">Dynamics 365 and Node.js integration using the Web API</a>. Here is Python code that demonstrates equivalent functionality to query contacts and display their information:</p>
<pre><code>import requests
import json

#set these values to retrieve the oauth token
crmorg = 'https://CRMORG.crm.dynamics.com' #base url for crm org
clientid = '00000000-0000-0000-0000-000000000000' #application client id
username = 'xxxxxx@xxxxxxxx' #username
userpassword = 'xxxxxxxx' #password
tokenendpoint = 'https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000/oauth2/token' #oauth token endpoint

#set these values to query your crm data
crmwebapi = 'https://CRMORG.api.crm.dynamics.com/api/data/v8.2' #full path to web api endpoint
crmwebapiquery = '/contacts?$select=fullname,contactid' #web api query (include leading /)

#build the authorization token request
tokenpost = {
    'client_id':clientid,
    'resource':crmorg,
    'username':username,
    'password':userpassword,
    'grant_type':'password'
}

#make the token request
tokenres = requests.post(tokenendpoint, data=tokenpost)

#set accesstoken variable to empty string
accesstoken = ''

#extract the access token
try:
    accesstoken = tokenres.json()['access_token']
except(KeyError):
    #handle any missing key errors
    print('Could not get access token')

#if we have an accesstoken
if(accesstoken!=''):
    #prepare the crm request headers
    crmrequestheaders = {
        'Authorization': 'Bearer ' + accesstoken,
        'OData-MaxVersion': '4.0',
        'OData-Version': '4.0',
        'Accept': 'application/json',
        'Content-Type': 'application/json; charset=utf-8',
        'Prefer': 'odata.maxpagesize=500',
        'Prefer': 'odata.include-annotations=OData.Community.Display.V1.FormattedValue'
    }

    #make the crm request
    crmres = requests.get(crmwebapi+crmwebapiquery, headers=crmrequestheaders)

    try:
        #get the response json
        crmresults = crmres.json()

        #loop through it
        for x in crmresults['value']:
            print (x['fullname'] + ' - ' + x['contactid'])
    except KeyError:
        #handle any missing key errors
        print('Could not parse CRM results')
</code></pre>
<p>This assumes you have configured an application in Azure Active Directory and know its client id and the Azure AD tenant id just like in my earlier Node.js example.</p>
<p>When I run this from my local PC against a my Dynamics 365 org with sample data installed, I get this output:<br>
<img src="https://alexanderdevelopment.net/content/images/2016/11/pythonw_2016-11-27_09-06-34.png#img-thumbnail" alt="Dynamics 365 and Python integration using the Web API"></p>
<p>Like my earlier Node.js example, this Python version isn't fancy, but it shows how easy it is to authenticate to Dynamics 365 and retrieve data without requiring special libraries.</p>
</div>]]></content:encoded></item></channel></rss>