Introduction

One of the aims for this project was to create a pipeline that foregoes saving individual ‘data’ files (e.g. csv, xml, json, txt) to the computer and instead uses a database to store the data. After reading over this great article that gives a good overview of SQLite, PostgreSQL, and MySQL, I decided on MySQL to handle my database. Also, my favorite Udemy instructor, Colt Steele, had a MySQL course on sale The Ultimate MySQL Bootcamp: Go from SQL Beginner to Expert that I highly recommend.

Goal

Setup MySQL database locally and connect Python and R to the databases directly.

Steps

  1. Install MySQL
  2. Create the Database
  3. Connect Python to MySQL with SQLAlchemy and MySQLdb
  4. Connect R to MySQL with odbc

 

Step 1: Install MySQL

I followed these instructions to download MySQL for my operating system (Windows). I chose the ‘Developer Default’ setup type to get the server, the MySQL Workbench, and the odbc/MySQL connector. If you are a pro at MySQL, you may not need the Workbench, but the other two are essential.

Under the ‘Type and Networking’ section, I chose the ‘Standalone MySQL Server/ Classic MySQL Replication. I left the default config type: development machine, set the ’root’ password and applied the configuration.

Step 2: Create the MySQL Database

We need to run some SQL commands. I usually use the MySQL workbench to run the MySQL statements but the GUI is a little slow so if you can manage it, using the MySQL Command Line Client will be more responsive.

First let’s create the database and call it bikeometers_db.

CREATE DATABASE bikeometers_db;

Now that we have the database created, let’s connect it to Python.

Step 3: Connect Python to MySQL

We will need to install the Python modules: SQLAlchemy and MySQLdb

If you are confused about how to install Python modules, this is a great article.

For me, I don’t have stand-alone Python installed on Windows, I just have my conda environment installed from the Anaconda Suite, so using python from the Windows command line is out of the question. Instead, I can type ‘pip install SQLAlchemy mysqlclient’ directly from my Spyder IDE and it will install the modules SQLAlchemy and MySQLdb respectively.

Next, we will import the two modules into Python: SQLAlchemy and the built-in Python module getpass which we will use to enter our database password.

from sqlalchemy import create_engine
from getpass import getpass

Now, let’s make the connection from Python to the database.

Using the SQLAlchemy create_engine function, pass in the name of your database as an f-string like I do below.

Note: below is the how the function is supposed to look when using Python. However, to create this post I’m actually using R and executing the Python code using an R library called ‘reticulate’ and I couldn’t get the the python module getpass to work. However, I’ve tried this code in Python and it works like a charm.

engine = create_engine(f'mysql+mysqldb://{input("Enter username: ")}:{getpass("Enter password: ")}@localhost/bikeometers_db')

As an alternative to securely entering your password each time you connect using the code above, I’ll use the Python module os to import my Environment Variables arlington_user and arlington_password which wont be saved in this code, protecting my database credentials.

import os
# Workaround to get this document to Knit
engine = create_engine(f'mysql+mysqldb://{os.environ["arlington_user"]}:{os.environ["arlington_password"]}@localhost/bikeometers_db', echo=False)

However, SQLAlchemy is ‘lazy’ and won’t actually try to make the connection until we explicitly tell it. So, the below code will test the connection and throw an error if a connection cannot be made.

engine.connect()
## <sqlalchemy.engine.base.Connection object at 0x0000000035336080>

If a ‘sqlalchemy.engine.base.Connection object’ is returned, that means you have successfully connected to your database!

Let’s make our first request from Python to MySQL.

First, we will assign the connection to the variable ‘con’.

con = engine.connect()

Next, we will use the execute method from SQLAlchemy on our ‘con’ variable and pass in a SQL command as an argument.

databases = con.execute('SHOW DATABASES;')
databases
## <sqlalchemy.engine.cursor.LegacyCursorResult object at 0x0000000035343240>

We can look inside our ‘cursor’ object to see the data using a for loop.

for item in databases:
  print(item[0])
## bikeometers_db
## counts
## information_schema
## mysql
## performance_schema
## sys

These are the databases currently on my computer.

When we are done with our SQL commands, best practice is to close the connection with the following code.

engine.dispose()

Great! Now that Python can talk to MySQL, we will connect R to MySQL.

Step 4: Connect R to MySQL

RStudio has a great website about connecting R and MySQL here that you can use if you are having trouble making the connection. I’ll outline the steps I used below.

In Step 1 of this post, we installed the MySQL/odbc Connector when we installed MySQL. If you didn’t install the connector, you can find it here. After installation, you will want to restart your computer to ensure it is properly recognized.

Once installed, make sure that both the odbc and DBI packages are installed in R.

First, we will load the odbc and DBI package in R.

library(odbc)
library(DBI)

Next, we will assign the connection to the variable ‘con’. You will notice that another password package is used to enter your database password. This ‘rstudioapi::askForPassword’ code will call the askForPassword function inside the rstudioapi package.

con <- dbConnect(odbc::odbc(), .connection_string = "Driver={MySQL ODBC 8.0 Unicode Driver};", 
                 server = "localhost", db = "bikeometers_db", user = readline(prompt = 'Enter user name: '), password = rstudioapi::askForPassword("Database password"))

However, I ran into a similar problem as before in that when ‘building’ this website, I can’t use the askForPassword R library. So again, I have saved my database login credentials in as Environment Variables. I can now ‘build’ this post without problems and subsequently publish the code without exposing my credentials. To learn more about my decision to use Enviornment Variables, see my post Publish Rmarkdown Documents With Database Connections Without Exposing Credentials

con <- dbConnect(odbc::odbc(), .connection_string = "Driver={MySQL ODBC 8.0 Unicode Driver};", 
                 server = "localhost", db = "bikeometers_db", user = Sys.getenv("arlington_user"), password = Sys.getenv("arlington_password"))

If you are using RStudio, and your connection is successful, you will now see a list of your MySQL databases in your ‘Connections’ tab.

Let’s send a MySQL Command from R to MySQL to confirm the two can communicate.

We will use R to send the same MySQL command as we did above using Python.

sql_cmd <- "SHOW DATABASES;"
bikeometers_db <- dbGetQuery(con, sql_cmd)
bikeometers_db
##             Database
## 1     bikeometers_db
## 2             counts
## 3 information_schema
## 4              mysql
## 5 performance_schema
## 6                sys

If you see a table of your databases, congratulations!

In the next post, we will save the data we pull from the Bike Arlington API into our MySQL database.

LS0tDQp0aXRsZTogIlZpc3VhbGl6aW5nIEFybGluZ3RvbiBCaWtvbWV0ZXJzIg0Kc3VidGl0bGU6ICJQYXJ0IDM6IFNldHVwIFlvdXIgRGF0YWJhc2UgYW5kIENvbm5lY3Rpb25zIg0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50OiANCiAgICB0b2M6IHllcw0KICAgIHRvY19kZXB0aDogMg0KICAgIHRvY19mbG9hdDogeWVzDQogICAgaGlnaGxpZ2h0OiB6ZW5idXJuDQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KICAgIGluY2x1ZGVzOg0KICAgICAgaW5faGVhZGVyOiBoZWFkZXIuaHRtbA0KLS0tDQoNClwgDQpcIA0KDQojIEludHJvZHVjdGlvbg0KDQpPbmUgb2YgdGhlIGFpbXMgZm9yIHRoaXMgcHJvamVjdCB3YXMgdG8gY3JlYXRlIGEgcGlwZWxpbmUgdGhhdCBmb3JlZ29lcyBzYXZpbmcgaW5kaXZpZHVhbCAnZGF0YScgZmlsZXMgKGUuZy4gY3N2LCB4bWwsIGpzb24sIHR4dCkgdG8gdGhlIGNvbXB1dGVyIGFuZCBpbnN0ZWFkIHVzZXMgYSBkYXRhYmFzZSB0byBzdG9yZSB0aGUgZGF0YS4gQWZ0ZXIgcmVhZGluZyBvdmVyIFt0aGlzIGdyZWF0IGFydGljbGVdKGh0dHBzOi8vcmVhbHB5dGhvbi5jb20vcHl0aG9uLW15c3FsLykgdGhhdCBnaXZlcyBhIGdvb2Qgb3ZlcnZpZXcgb2YgU1FMaXRlLCBQb3N0Z3JlU1FMLCBhbmQgTXlTUUwsIEkgZGVjaWRlZCBvbiBNeVNRTCB0byBoYW5kbGUgbXkgZGF0YWJhc2UuIEFsc28sIG15IGZhdm9yaXRlIFVkZW15IGluc3RydWN0b3IsIENvbHQgU3RlZWxlLCBoYWQgYSBNeVNRTCBjb3Vyc2Ugb24gc2FsZSBbVGhlIFVsdGltYXRlIE15U1FMIEJvb3RjYW1wOiBHbyBmcm9tIFNRTCBCZWdpbm5lciB0byBFeHBlcnRdKGh0dHBzOi8vd3d3LnVkZW15LmNvbS9jb3Vyc2UvdGhlLXVsdGltYXRlLW15c3FsLWJvb3RjYW1wLWdvLWZyb20tc3FsLWJlZ2lubmVyLXRvLWV4cGVydC8pIHRoYXQgSSBoaWdobHkgcmVjb21tZW5kLg0KDQojIyBHb2FsDQoNClNldHVwIE15U1FMIGRhdGFiYXNlIGxvY2FsbHkgYW5kIGNvbm5lY3QgUHl0aG9uIGFuZCBSIHRvIHRoZSBkYXRhYmFzZXMgZGlyZWN0bHkuDQoNCiMjIFN0ZXBzDQoxLiBJbnN0YWxsIE15U1FMDQoyLiBDcmVhdGUgdGhlIERhdGFiYXNlDQozLiBDb25uZWN0IFB5dGhvbiB0byBNeVNRTCB3aXRoIFNRTEFsY2hlbXkgYW5kIE15U1FMZGINCjQuIENvbm5lY3QgUiB0byBNeVNRTCB3aXRoIG9kYmMNCg0KXCANCg0KIyBTdGVwIDE6IEluc3RhbGwgTXlTUUwNCg0KSSBmb2xsb3dlZCBbdGhlc2UgaW5zdHJ1Y3Rpb25zXShodHRwczovL2Rldi5teXNxbC5jb20vZG9jL215c3FsLWluc3RhbGxhdGlvbi1leGNlcnB0LzUuNy9lbi9pbnN0YWxsaW5nLmh0bWwpIHRvIGRvd25sb2FkIE15U1FMIGZvciBteSBvcGVyYXRpbmcgc3lzdGVtIChXaW5kb3dzKS4gSSBjaG9zZSB0aGUgJ0RldmVsb3BlciBEZWZhdWx0JyBzZXR1cCB0eXBlIHRvIGdldCB0aGUgc2VydmVyLCB0aGUgTXlTUUwgV29ya2JlbmNoLCBhbmQgdGhlIG9kYmMvTXlTUUwgY29ubmVjdG9yLiBJZiB5b3UgYXJlIGEgcHJvIGF0IE15U1FMLCB5b3UgbWF5IG5vdCBuZWVkIHRoZSBXb3JrYmVuY2gsIGJ1dCB0aGUgb3RoZXIgdHdvIGFyZSBlc3NlbnRpYWwuIA0KDQpVbmRlciB0aGUgJ1R5cGUgYW5kIE5ldHdvcmtpbmcnIHNlY3Rpb24sIEkgY2hvc2UgdGhlICdTdGFuZGFsb25lIE15U1FMIFNlcnZlci8gQ2xhc3NpYyBNeVNRTCBSZXBsaWNhdGlvbi4gSSBsZWZ0IHRoZSBkZWZhdWx0ICpjb25maWcgdHlwZTogZGV2ZWxvcG1lbnQgbWFjaGluZSosIHNldCB0aGUgJ3Jvb3QnIHBhc3N3b3JkIGFuZCBhcHBsaWVkIHRoZSBjb25maWd1cmF0aW9uLiANCg0KIyBTdGVwIDI6IENyZWF0ZSB0aGUgTXlTUUwgRGF0YWJhc2UNCg0KV2UgbmVlZCB0byBydW4gc29tZSBTUUwgY29tbWFuZHMuIEkgdXN1YWxseSB1c2UgdGhlIE15U1FMIHdvcmtiZW5jaCB0byBydW4gdGhlIE15U1FMIHN0YXRlbWVudHMgYnV0IHRoZSBHVUkgaXMgYSBsaXR0bGUgc2xvdyBzbyBpZiB5b3UgY2FuIG1hbmFnZSBpdCwgdXNpbmcgdGhlICpNeVNRTCBDb21tYW5kIExpbmUgQ2xpZW50KiB3aWxsIGJlIG1vcmUgcmVzcG9uc2l2ZS4NCg0KRmlyc3QgbGV0J3MgY3JlYXRlIHRoZSBkYXRhYmFzZSBhbmQgY2FsbCBpdCAqYmlrZW9tZXRlcnNfZGIqLg0KDQpgYGB7c3FsIGV2YWw9RkFMU0V9DQpDUkVBVEUgREFUQUJBU0UgYmlrZW9tZXRlcnNfZGI7DQpgYGANCg0KTm93IHRoYXQgd2UgaGF2ZSB0aGUgZGF0YWJhc2UgY3JlYXRlZCwgbGV0J3MgY29ubmVjdCBpdCB0byBQeXRob24uDQoNCiMgU3RlcCAzOiBDb25uZWN0IFB5dGhvbiB0byBNeVNRTA0KDQpXZSB3aWxsIG5lZWQgdG8gaW5zdGFsbCB0aGUgUHl0aG9uIG1vZHVsZXM6IFNRTEFsY2hlbXkgYW5kIE15U1FMZGINCg0KSWYgeW91IGFyZSBjb25mdXNlZCBhYm91dCBob3cgdG8gaW5zdGFsbCBQeXRob24gbW9kdWxlcywgW3RoaXNdKGh0dHBzOi8vcmVhbHB5dGhvbi5jb20vd2hhdC1pcy1waXAvKSBpcyBhIGdyZWF0IGFydGljbGUuDQoNCkZvciBtZSwgSSBkb24ndCBoYXZlIHN0YW5kLWFsb25lIFB5dGhvbiBpbnN0YWxsZWQgb24gV2luZG93cywgSSBqdXN0IGhhdmUgbXkgY29uZGEgZW52aXJvbm1lbnQgaW5zdGFsbGVkIGZyb20gdGhlIEFuYWNvbmRhIFN1aXRlLCBzbyB1c2luZyBweXRob24gZnJvbSB0aGUgV2luZG93cyBjb21tYW5kIGxpbmUgaXMgb3V0IG9mIHRoZSBxdWVzdGlvbi4gSW5zdGVhZCwgSSBjYW4gdHlwZSAncGlwIGluc3RhbGwgU1FMQWxjaGVteSBteXNxbGNsaWVudCcgZGlyZWN0bHkgZnJvbSBteSBTcHlkZXIgSURFIGFuZCBpdCB3aWxsIGluc3RhbGwgdGhlIG1vZHVsZXMgKipTUUxBbGNoZW15KiogYW5kICoqTXlTUUxkYioqIHJlc3BlY3RpdmVseS4NCg0KTmV4dCwgd2Ugd2lsbCBpbXBvcnQgdGhlIHR3byBtb2R1bGVzIGludG8gUHl0aG9uOiAqKlNRTEFsY2hlbXkqKiBhbmQgdGhlIGJ1aWx0LWluIFB5dGhvbiBtb2R1bGUgKipnZXRwYXNzKiogd2hpY2ggd2Ugd2lsbCB1c2UgdG8gZW50ZXIgb3VyIGRhdGFiYXNlIHBhc3N3b3JkLg0KDQpgYGB7cHl0aG9ufQ0KZnJvbSBzcWxhbGNoZW15IGltcG9ydCBjcmVhdGVfZW5naW5lDQpmcm9tIGdldHBhc3MgaW1wb3J0IGdldHBhc3MNCmBgYA0KDQpOb3csIGxldCdzIG1ha2UgdGhlIGNvbm5lY3Rpb24gZnJvbSBQeXRob24gdG8gdGhlIGRhdGFiYXNlLg0KDQpVc2luZyB0aGUgU1FMQWxjaGVteSAqKmNyZWF0ZV9lbmdpbmUqKiBmdW5jdGlvbiwgcGFzcyBpbiB0aGUgbmFtZSBvZiB5b3VyIGRhdGFiYXNlIGFzIGFuIFtmLXN0cmluZ10oaHR0cHM6Ly9yZWFscHl0aG9uLmNvbS9weXRob24tZi1zdHJpbmdzLykgbGlrZSBJIGRvIGJlbG93Lg0KDQpOb3RlOiBiZWxvdyBpcyB0aGUgaG93IHRoZSBmdW5jdGlvbiBpcyBzdXBwb3NlZCB0byBsb29rIHdoZW4gdXNpbmcgUHl0aG9uLiBIb3dldmVyLCB0byBjcmVhdGUgdGhpcyBwb3N0IEknbSBhY3R1YWxseSB1c2luZyBSIGFuZCBleGVjdXRpbmcgdGhlIFB5dGhvbiBjb2RlIHVzaW5nIGFuIFIgbGlicmFyeSBjYWxsZWQgJ3JldGljdWxhdGUnIGFuZCBJIGNvdWxkbid0IGdldCB0aGUgdGhlIHB5dGhvbiBtb2R1bGUgKipnZXRwYXNzKiogdG8gd29yay4gSG93ZXZlciwgSSd2ZSB0cmllZCB0aGlzIGNvZGUgaW4gUHl0aG9uIGFuZCBpdCB3b3JrcyBsaWtlIGEgY2hhcm0uDQoNCmBgYHtweXRob24gZXZhbD1GQUxTRX0NCmVuZ2luZSA9IGNyZWF0ZV9lbmdpbmUoZidteXNxbCtteXNxbGRiOi8ve2lucHV0KCJFbnRlciB1c2VybmFtZTogIil9OntnZXRwYXNzKCJFbnRlciBwYXNzd29yZDogIil9QGxvY2FsaG9zdC9iaWtlb21ldGVyc19kYicpDQpgYGANCg0KQXMgYW4gYWx0ZXJuYXRpdmUgdG8gc2VjdXJlbHkgZW50ZXJpbmcgeW91ciBwYXNzd29yZCBlYWNoIHRpbWUgeW91IGNvbm5lY3QgdXNpbmcgdGhlIGNvZGUgYWJvdmUsIEknbGwgdXNlIHRoZSBQeXRob24gbW9kdWxlICpvcyogdG8gaW1wb3J0IG15ICpFbnZpcm9ubWVudCBWYXJpYWJsZXMqICoqYXJsaW5ndG9uX3VzZXIqKiBhbmQgKiphcmxpbmd0b25fcGFzc3dvcmQqKiB3aGljaCB3b250IGJlIHNhdmVkIGluIHRoaXMgY29kZSwgcHJvdGVjdGluZyBteSBkYXRhYmFzZSBjcmVkZW50aWFscy4gDQoNCmBgYHtweXRob259DQppbXBvcnQgb3MNCiMgV29ya2Fyb3VuZCB0byBnZXQgdGhpcyBkb2N1bWVudCB0byBLbml0DQplbmdpbmUgPSBjcmVhdGVfZW5naW5lKGYnbXlzcWwrbXlzcWxkYjovL3tvcy5lbnZpcm9uWyJhcmxpbmd0b25fdXNlciJdfTp7b3MuZW52aXJvblsiYXJsaW5ndG9uX3Bhc3N3b3JkIl19QGxvY2FsaG9zdC9iaWtlb21ldGVyc19kYicsIGVjaG89RmFsc2UpDQpgYGANCg0KSG93ZXZlciwgU1FMQWxjaGVteSBpcyAnbGF6eScgYW5kIHdvbid0IGFjdHVhbGx5IHRyeSB0byBtYWtlIHRoZSBjb25uZWN0aW9uIHVudGlsIHdlIGV4cGxpY2l0bHkgdGVsbCBpdC4gU28sIHRoZSBiZWxvdyBjb2RlIHdpbGwgdGVzdCB0aGUgY29ubmVjdGlvbiBhbmQgdGhyb3cgYW4gZXJyb3IgaWYgYSBjb25uZWN0aW9uIGNhbm5vdCBiZSBtYWRlLg0KDQpgYGB7cHl0aG9uIH0NCmVuZ2luZS5jb25uZWN0KCkNCmBgYA0KDQpJZiBhIConc3FsYWxjaGVteS5lbmdpbmUuYmFzZS5Db25uZWN0aW9uIG9iamVjdCcqIGlzIHJldHVybmVkLCB0aGF0IG1lYW5zIHlvdSBoYXZlIHN1Y2Nlc3NmdWxseSBjb25uZWN0ZWQgdG8geW91ciBkYXRhYmFzZSENCg0KTGV0J3MgbWFrZSBvdXIgZmlyc3QgcmVxdWVzdCBmcm9tIFB5dGhvbiB0byBNeVNRTC4NCg0KRmlyc3QsIHdlIHdpbGwgYXNzaWduIHRoZSBjb25uZWN0aW9uIHRvIHRoZSB2YXJpYWJsZSAnY29uJy4NCmBgYHtweXRob259DQpjb24gPSBlbmdpbmUuY29ubmVjdCgpDQpgYGANCk5leHQsIHdlIHdpbGwgdXNlIHRoZSAqKmV4ZWN1dGUqKiBtZXRob2QgZnJvbSBTUUxBbGNoZW15IG9uIG91ciAnY29uJyB2YXJpYWJsZSBhbmQgcGFzcyBpbiBhIFNRTCBjb21tYW5kIGFzIGFuIGFyZ3VtZW50Lg0KYGBge3B5dGhvbiB9DQpkYXRhYmFzZXMgPSBjb24uZXhlY3V0ZSgnU0hPVyBEQVRBQkFTRVM7JykNCmRhdGFiYXNlcw0KYGBgDQpXZSBjYW4gbG9vayBpbnNpZGUgb3VyICdjdXJzb3InIG9iamVjdCB0byBzZWUgdGhlIGRhdGEgdXNpbmcgYSAqZm9yIGxvb3AqLg0KYGBge3B5dGhvbiB9DQpmb3IgaXRlbSBpbiBkYXRhYmFzZXM6DQogIHByaW50KGl0ZW1bMF0pDQoNCmBgYA0KVGhlc2UgYXJlIHRoZSBkYXRhYmFzZXMgY3VycmVudGx5IG9uIG15IGNvbXB1dGVyLg0KDQpXaGVuIHdlIGFyZSBkb25lIHdpdGggb3VyIFNRTCBjb21tYW5kcywgYmVzdCBwcmFjdGljZSBpcyB0byBjbG9zZSB0aGUgY29ubmVjdGlvbiB3aXRoIHRoZSBmb2xsb3dpbmcgY29kZS4NCg0KYGBge3B5dGhvbiB9DQplbmdpbmUuZGlzcG9zZSgpDQpgYGANCg0KR3JlYXQhIE5vdyB0aGF0IFB5dGhvbiBjYW4gdGFsayB0byBNeVNRTCwgd2Ugd2lsbCBjb25uZWN0IFIgdG8gTXlTUUwuDQoNCiMgU3RlcCA0OiBDb25uZWN0IFIgdG8gTXlTUUwNCg0KUlN0dWRpbyBoYXMgYSBncmVhdCB3ZWJzaXRlIGFib3V0IGNvbm5lY3RpbmcgUiBhbmQgTXlTUUwgW2hlcmVdKGh0dHBzOi8vZGIucnN0dWRpby5jb20vZGF0YWJhc2VzL215LXNxbC8pIHRoYXQgeW91IGNhbiB1c2UgaWYgeW91IGFyZSBoYXZpbmcgdHJvdWJsZSBtYWtpbmcgdGhlIGNvbm5lY3Rpb24uIEknbGwgb3V0bGluZSB0aGUgc3RlcHMgSSB1c2VkIGJlbG93Lg0KDQpJbiBTdGVwIDEgb2YgdGhpcyBwb3N0LCB3ZSBpbnN0YWxsZWQgdGhlIE15U1FML29kYmMgQ29ubmVjdG9yIHdoZW4gd2UgaW5zdGFsbGVkIE15U1FMLiBJZiB5b3UgZGlkbid0IGluc3RhbGwgdGhlIGNvbm5lY3RvciwgeW91IGNhbiBmaW5kIGl0IFtoZXJlXShodHRwczovL2Rldi5teXNxbC5jb20vZG93bmxvYWRzL2Nvbm5lY3Rvci9vZGJjLykuIEFmdGVyIGluc3RhbGxhdGlvbiwgeW91IHdpbGwgd2FudCB0byByZXN0YXJ0IHlvdXIgY29tcHV0ZXIgdG8gZW5zdXJlIGl0IGlzIHByb3Blcmx5IHJlY29nbml6ZWQuDQoNCk9uY2UgaW5zdGFsbGVkLCBtYWtlIHN1cmUgdGhhdCBib3RoIHRoZSAqKm9kYmMqKiBhbmQgKipEQkkqKiBwYWNrYWdlcyBhcmUgaW5zdGFsbGVkIGluIFIuDQoNCkZpcnN0LCB3ZSB3aWxsIGxvYWQgdGhlICoqb2RiYyoqIGFuZCAqKkRCSSoqIHBhY2thZ2UgaW4gUi4NCmBgYHtyIH0NCmxpYnJhcnkob2RiYykNCmxpYnJhcnkoREJJKQ0KYGBgDQoNCk5leHQsIHdlIHdpbGwgYXNzaWduIHRoZSBjb25uZWN0aW9uIHRvIHRoZSB2YXJpYWJsZSAnY29uJy4gWW91IHdpbGwgbm90aWNlIHRoYXQgYW5vdGhlciBwYXNzd29yZCBwYWNrYWdlIGlzIHVzZWQgdG8gZW50ZXIgeW91ciBkYXRhYmFzZSBwYXNzd29yZC4gVGhpcyAqJ3JzdHVkaW9hcGk6OmFza0ZvclBhc3N3b3JkJyogY29kZSB3aWxsIGNhbGwgdGhlICoqYXNrRm9yUGFzc3dvcmQqKiBmdW5jdGlvbiBpbnNpZGUgdGhlICpyc3R1ZGlvYXBpKiBwYWNrYWdlLiANCg0KYGBge3IgZXZhbD1GQUxTRX0NCmNvbiA8LSBkYkNvbm5lY3Qob2RiYzo6b2RiYygpLCAuY29ubmVjdGlvbl9zdHJpbmcgPSAiRHJpdmVyPXtNeVNRTCBPREJDIDguMCBVbmljb2RlIERyaXZlcn07IiwgDQogICAgICAgICAgICAgICAgIHNlcnZlciA9ICJsb2NhbGhvc3QiLCBkYiA9ICJiaWtlb21ldGVyc19kYiIsIHVzZXIgPSByZWFkbGluZShwcm9tcHQgPSAnRW50ZXIgdXNlciBuYW1lOiAnKSwgcGFzc3dvcmQgPSByc3R1ZGlvYXBpOjphc2tGb3JQYXNzd29yZCgiRGF0YWJhc2UgcGFzc3dvcmQiKSkNCmBgYA0KDQpIb3dldmVyLCBJIHJhbiBpbnRvIGEgc2ltaWxhciBwcm9ibGVtIGFzIGJlZm9yZSBpbiB0aGF0IHdoZW4gJ2J1aWxkaW5nJyB0aGlzIHdlYnNpdGUsIEkgY2FuJ3QgdXNlIHRoZSAqYXNrRm9yUGFzc3dvcmQqIFIgbGlicmFyeS4gU28gYWdhaW4sIEkgaGF2ZSBzYXZlZCBteSBkYXRhYmFzZSBsb2dpbiBjcmVkZW50aWFscyBpbiBhcyAqRW52aXJvbm1lbnQgVmFyaWFibGVzKi4gSSBjYW4gbm93ICdidWlsZCcgdGhpcyBwb3N0IHdpdGhvdXQgcHJvYmxlbXMgYW5kIHN1YnNlcXVlbnRseSBwdWJsaXNoIHRoZSBjb2RlIHdpdGhvdXQgZXhwb3NpbmcgbXkgY3JlZGVudGlhbHMuIFRvIGxlYXJuIG1vcmUgYWJvdXQgbXkgZGVjaXNpb24gdG8gdXNlIEVudmlvcm5tZW50IFZhcmlhYmxlcywgc2VlIG15IHBvc3QgW1B1Ymxpc2ggUm1hcmtkb3duIERvY3VtZW50cyBXaXRoIERhdGFiYXNlIENvbm5lY3Rpb25zIFdpdGhvdXQgRXhwb3NpbmcgQ3JlZGVudGlhbHNdKGh0dHBzOi8vbmF0aGFuc3Byb2plY3RzLmNvbS9ub3RlX2tuaXRfcm1hcmtkb3duX2RiLmh0bWwpDQoNCmBgYHtyIH0NCmNvbiA8LSBkYkNvbm5lY3Qob2RiYzo6b2RiYygpLCAuY29ubmVjdGlvbl9zdHJpbmcgPSAiRHJpdmVyPXtNeVNRTCBPREJDIDguMCBVbmljb2RlIERyaXZlcn07IiwgDQogICAgICAgICAgICAgICAgIHNlcnZlciA9ICJsb2NhbGhvc3QiLCBkYiA9ICJiaWtlb21ldGVyc19kYiIsIHVzZXIgPSBTeXMuZ2V0ZW52KCJhcmxpbmd0b25fdXNlciIpLCBwYXNzd29yZCA9IFN5cy5nZXRlbnYoImFybGluZ3Rvbl9wYXNzd29yZCIpKQ0KYGBgDQoNCklmIHlvdSBhcmUgdXNpbmcgUlN0dWRpbywgYW5kIHlvdXIgY29ubmVjdGlvbiBpcyBzdWNjZXNzZnVsLCB5b3Ugd2lsbCBub3cgc2VlIGEgbGlzdCBvZiB5b3VyIE15U1FMIGRhdGFiYXNlcyBpbiB5b3VyICdDb25uZWN0aW9ucycgdGFiLg0KDQpMZXQncyBzZW5kIGEgTXlTUUwgQ29tbWFuZCBmcm9tIFIgdG8gTXlTUUwgdG8gY29uZmlybSB0aGUgdHdvIGNhbiBjb21tdW5pY2F0ZS4NCg0KV2Ugd2lsbCB1c2UgUiB0byBzZW5kIHRoZSBzYW1lIE15U1FMIGNvbW1hbmQgYXMgd2UgZGlkIGFib3ZlIHVzaW5nIFB5dGhvbi4gDQpgYGB7ciB9DQpzcWxfY21kIDwtICJTSE9XIERBVEFCQVNFUzsiDQpiaWtlb21ldGVyc19kYiA8LSBkYkdldFF1ZXJ5KGNvbiwgc3FsX2NtZCkNCmJpa2VvbWV0ZXJzX2RiDQpgYGANCklmIHlvdSBzZWUgYSB0YWJsZSBvZiB5b3VyIGRhdGFiYXNlcywgY29uZ3JhdHVsYXRpb25zIQ0KDQpJbiB0aGUgW25leHQgcG9zdF0oaHR0cHM6Ly9uYXRoYW5zcHJvamVjdHMuY29tL3BhcnRfNF9zYXZlX3RoZV9kYXRhX2luX2FfbXlzcWxfZGF0YWJhc2UuaHRtbCksIHdlIHdpbGwgc2F2ZSB0aGUgZGF0YSB3ZSBwdWxsIGZyb20gdGhlIEJpa2UgQXJsaW5ndG9uIEFQSSBpbnRvIG91ciBNeVNRTCBkYXRhYmFzZS4gDQo=