<?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[Nightly Closures]]></title><description><![CDATA[var headline = (function () { alert('Good night'); })();]]></description><link>http://104.236.78.148/</link><generator>Ghost 0.11</generator><lastBuildDate>Fri, 27 Mar 2026 22:05:49 GMT</lastBuildDate><atom:link href="http://104.236.78.148/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[This one simple trick lets you stream with dbt and Databricks]]></title><description><![CDATA[<p>Are you using <a href="https://www.databricks.com/">Databricks</a> and want to use <a href="https://docs.getdbt.com/">dbt</a>, but you're not sure how to do it with all of your big data? Like, dbt can't seem to stream, and you need to stream because anything else is too expensive or too much work or too flaky? </p>

<p>I've used this</p>]]></description><link>http://104.236.78.148/2025/11/19/this-one-simple-trick-lets-you-stream-with-dbt-and-databricks/</link><guid isPermaLink="false">a3e473cf-9d65-499a-b072-7380dd416b44</guid><dc:creator><![CDATA[Kyle Valade]]></dc:creator><pubDate>Wed, 19 Nov 2025 20:27:01 GMT</pubDate><content:encoded><![CDATA[<p>Are you using <a href="https://www.databricks.com/">Databricks</a> and want to use <a href="https://docs.getdbt.com/">dbt</a>, but you're not sure how to do it with all of your big data? Like, dbt can't seem to stream, and you need to stream because anything else is too expensive or too much work or too flaky? </p>

<p>I've used this pattern and seen it used across several teams of data people reading through petabytes-ish of data.</p>

<p>dbt doesn't natively support streaming, unless you use Databricks streaming tables or materialized views, which I don't really like to do because they have always failed me eventually. They start out great, but as soon as something happens that you want to debug, or you need to rename your table or something, then you're SOL. So I don't use them - I use plain old streaming. (No offense to any Databricks folks)</p>

<p>Some people might not realize that when your dbt python notebook code gets run, it's really just uploaded as a notebook. The default dbt function (<code>model()</code>), takes the result and feeds it into a table.</p>

<p>SO since it's just a notebook, you <em>can</em> stream...it's just a matter of getting dbt to not overwrite your existing table's data, and checkpointing, making sure you get alerts if things fail, setting timeouts, etc. And that turns out to be pretty simple. </p>

<p>So the one simple trick is....<del>hey, have you considered supplements?</del></p>

<p>Create an empty dataframe with the schema you want, and have <code>model()</code> return that empty dataframe so that NOTHING is appended to your table. </p>

<p>Use a <a href="https://docs.getdbt.com/reference/resource-configs/databricks-configs#python-submission-methods">workflow_job</a> because it breaks you out of a dbt sandbox and lets you use everything from Databricks jobs. Otherwise they jam you into these one-time runs. And because, shameless plug, I wrote most of it, though as an act of frustration, and with a bunch of help from Databricks (shoutout Ben).</p>

<p>Is that just a wall of text? I hope not. I'll add fun memes one day. Until then, check out this wall of code.</p>

<p>This is a simple example. It streams a log file table and appends new entries to a new table. Substitute log file for telemetry or other things you might be interested in. I'm going to be honest - I haven't run this exact code, so let me know if anything is wrong.</p>

<pre><code># COMMAND ----------

# MAGIC %pip install Pillow

# Just an example for pip installing...

dbutils.library.restartPython()

# COMMAND ----------

import pyspark.sql.types as T  
import pyspark.sql.functions as F


def get_or_create_checkpoint_location(dbt):  
    """Create checkpoint location for streaming query."""
    create_volume_query = f"CREATE VOLUME IF NOT EXISTS {dbt.this.database}.{dbt.this.schema}.checkpoints"
    print("Create volume query", create_volume_query)
    spark.sql(create_volume_query)
    return f"/Volumes/{dbt.this.database}/{dbt.this.schema}/checkpoints/{dbt.this.identifier}"

def run_stream(dbt):  
    """Run streaming query to process new log files."""
    checkpoint_location = get_or_create_checkpoint_location(dbt)
    output_location = str(dbt.this)

    log_files_df = (spark.readStream
      .format('delta')
      .option('ignoreChanges', 'true')
      .option('ignoreDeletes', 'true')
      .table('my_catalog.schema.log_files')
    )

    write_stream = (
      log_files_df.writeStream.format("delta")
      .outputMode('append')
      .option("checkpointLocation", checkpoint_location)
      .option("mergeSchema", "true")
      .trigger(availableNow = True)
      .start(output_location)
    )

def model(dbt, session):  
    """Main entry point for dbt model."""
    dbt.config(
        materialized='incremental',
        submission_method='workflow_job'
    )

    # Define output schema
    output_schema = T.StructType([
        T.StructField('log_entry_id', T.StringType(), False),
        T.StructField('log_file_id', T.IntegerType(), False),
    ])

    df = spark.createDataFrame(data=spark.sparkContext.emptyRDD(), schema=output_schema)

    if not dbt.is_incremental:
        # Create table if it doesn't exist
        df.write.saveAsTable(str(dbt.this), mode='append')

    # Run streaming query
    run_stream(dbt)

    return df
</code></pre>

<p>The yaml is going to be something like:</p>

<pre><code>version: 2

models:  
  - name: int_device_logs

    config:
#      cluster_id: afsfs-1232819-dsfbkjbs1  # Use this when developing
      python_job_config:
        timeout_seconds: 3600
        email_notifications: { on_failure: ["me@example.com"] }
        max_retries: 2

        name: my_workflow_name

        # Override settings for your model's dbt task. For instance, you can
        # change the task key
        additional_task_settings: { "task_key": "my_dbt_task" }

        # Define tasks to run before/after the model
        # This example assumes you have already uploaded a notebook to /my_notebook_path to perform optimize and vacuum
        post_hook_tasks:
          [
            {
              "depends_on": [{ "task_key": "my_dbt_task" }],
              "task_key": "OPTIMIZE_AND_VACUUM",
              "notebook_task":
                { "notebook_path": "/my_notebook_path", "source": "WORKSPACE" },
            },
          ]

        # Simplified structure, rather than having to specify permission separately for each user
        grants:
          view: [{ "group_name": "marketing-team" }]
          run: [{ "user_name": "other_user@example.com" }]
          manage: []
      job_cluster_config:
        spark_version: "16.4.x-scala2.12"
        node_type_id: "c2-standard-4"
        runtime_engine: "STANDARD"
        data_security_mode: "SINGLE_USER"
        single_user_name: "aghodsi@databricks.com"
        autoscale: { "min_workers": 1, "max_workers": 1 }
</code></pre>

<p>You can override pretty much anything in a job here.</p>

<p>If you want to pip install a package in your notebook, use <code># MAGIC %pip install Pillow</code> or whatever. If you go with the regular <code>%pip install my-package</code>, the arbitrarily vindictive dbt python parser is going to get you.</p>

<p>There are some fine people at dbt, but my feelings on the company are for another post - especially now that they are safely with fivetran instead of (insolvently?) independent.</p>]]></content:encoded></item><item><title><![CDATA[Automating tedium with the ServiceNow API]]></title><description><![CDATA[<p>I should have named this blog Enterprise <expletive>. But big companies are going to use ServiceNow whether or not HR likes handling onboarding tickets in the side-project you wrote in beautiful, idiomatic Go. But that doesn't mean that <em>you</em> personally need to log in every time a ticket comes your way.</expletive></p>]]></description><link>http://104.236.78.148/2024/01/12/automating-tedium-with-the-servicenow-api/</link><guid isPermaLink="false">ef46c63b-1e30-4238-9442-eff99c369796</guid><dc:creator><![CDATA[Kyle Valade]]></dc:creator><pubDate>Fri, 12 Jan 2024 17:25:07 GMT</pubDate><content:encoded><![CDATA[<p>I should have named this blog Enterprise <expletive>. But big companies are going to use ServiceNow whether or not HR likes handling onboarding tickets in the side-project you wrote in beautiful, idiomatic Go. But that doesn't mean that <em>you</em> personally need to log in every time a ticket comes your way.</expletive></p>

<p>So, say you want to automate whatever happens if someone files a ServiceNow ticket to, say, get access to your pristine Database - surely there's an API to facilitate that? Yes, there is. The API doesn't seem to be super well-documented, or if it is, that documentation is buried under layers of other ServiceNow jargon.</p>

<p>The API is organized kind of like a relational database, so it's very flexible, maybe to a fault. But it's not so bad once you get into it. There are a lot of undocumented attributes that can be accessed by dot-walking your way through real or imagined properties.</p>

<p>First, there is a "REST API Explorer" that you'll need an admin to grant you access to. It would be at a URL like this <a href="https://your-company.service-now.com/now/nav/ui/classic/params/target/%24restapi.do">https://your-company.service-now.com/now/nav/ui/classic/params/target/%24restapi.do</a>. You'll also want an API user + password...unfortunately there doesn't seem to be much in terms of permissions. An API user is able to do pretty much anything.</p>

<p>In the REST Explorer, you can explore all of the tables and make some sample requests. Paired with the Task List UI (where you can search for all tasks in ServiceNow), you can build the search params you want, then right-click the breadcrumbs and copy the params to use in the <code>sysparm_query</code> arg below.</p>

<p>My flow:</p>

<ul>
<li>Get all open tickets
<ul><li><code>/sc_task</code> to get the Catalog tasks (ie - SCTASK00123)</li>
<li>Can filter by category item - for example <code>?sysparm_query=assignment_group.name=Data Team Requests</code></li>
<li>Using the <code>sysparm_fields</code> param, make sure to also pull the <code>request_item.sys_id</code> because we'll need that below</li></ul></li>
<li>For each ticket, we need to populate the values for any custom created fields
<ul><li>This is where the real dot-walking comes in. Hit <code>/sc_item_option_mtom</code> with <code>sysparm_query= request_item.sys_id=&lt;request item id&gt;</code> and select the following fields:</li></ul></li>
</ul>

<pre><code>"sys_id",
"sc_item_option.value",
"sc_item_option.sys_id",
"sc_item_option.item_option_new.sys_id",
"sc_item_option.item_option_new.sys_name",
"sc_item_option.item_option_new.question_text",
"sc_item_option.item_option_new.question.value"
</code></pre>

<ul>
<li>For each of <em>those</em> fields, if those are a multiple choice question, the API will only return a sys_id as the response. That means you need to hit another endpoint to get the actual value. My implementation is pretty quick and dirty, so I'm just doing a nested loop, but I'm sure you could pre-populate these values if you cared.
<ul><li>Hit the <code>/question_choice</code> endpoint with the <code>sysparm_query=sys_id=&lt;that sys_id&gt;</code></li>
<li>Can combined multiple ids with an <code>OR</code> like <code>sysparm_query=sys_id=123ORsys_id=456</code></li></ul></li>
<li>PATCH the ticket to close with a comment</li>
</ul>

<pre><code>PATCH /sc_task/&lt;sys_id&gt;  
{
    "state": "3",
    "comments": "G'day"
}
</code></pre>

<p>Trying to specifically add a close note with <code>close_notes</code> didn't work but I figure it's close enough.</p>

<p>There - not so bad! There have surely been worse APIs in the history of the universe.</p>]]></content:encoded></item><item><title><![CDATA[Eternities of Gitlab CI pipeline trial + error...only run with changes]]></title><description><![CDATA[<p>I've spent many mind-numbing hours trying to get this Gitlab CI pipeline to work the way I want it to work. I'm not sure if I'm doing something wrong, or if GitlabCI is doing something wrong, but I'm becoming more and more convinced that it's GitlabCI.</p>

<p>Essentially, we have many</p>]]></description><link>http://104.236.78.148/2023/04/03/eternities-of-gitlab-ci-pipeline-changes-only-change-when-theres-a-change-in-a-certain-dir/</link><guid isPermaLink="false">c30ede3c-af90-4a0a-bea8-e971d1426536</guid><dc:creator><![CDATA[Kyle Valade]]></dc:creator><pubDate>Mon, 03 Apr 2023 23:22:30 GMT</pubDate><content:encoded><![CDATA[<p>I've spent many mind-numbing hours trying to get this Gitlab CI pipeline to work the way I want it to work. I'm not sure if I'm doing something wrong, or if GitlabCI is doing something wrong, but I'm becoming more and more convinced that it's GitlabCI.</p>

<p>Essentially, we have many different projects in our repo, separated by dirs. If nothing in a project changes, I don't want it to go through the CI process because it's a waste of time.</p>

<p>Luckily, GitlabCI solves for this...right...? I mean, yeah, you can define variables so they should work everywhere like you'd expect, right?</p>

<p>I started with something like this. Imagine like 10 of the <code>one-project--plan</code> all for different projects. I wanted to break out the <code>only:changes</code> part so that I don't have to repeat it everywhere.</p>

<pre><code>one-project--plan:  
  stage: terraform-plan
  extends:
    - .install-cli-and-assume-role
    - .terraform-plan-only
  variables:
    TF_DIR: path/to/my/project
    WORKSPACE_NAME: my-tf-project-workspace-one

.terraform-plan-only:
  script:
    - cd "${TF_DIR}"
    - terraform init
    - terraform plan
  except:
    - master
  only:
    changes:
      - '${TF_DIR}/**/*'
</code></pre>

<p>Great - that makes sense, right? Sure, but it doesn't work. Hardcode the path, though, and it works. Ok, that's weird, but at least we have a baseline to work from.</p>

<p>A <a href="https://gitlab.com/gitlab-org/gitlab/-/issues/8177#note_113091110">little digging</a> shows that <code>only</code> is compiled at a different time than the rest of it, so you can't use variables in <code>only/except</code>. The recommended approach is to switch to using <code>rules</code>. Rules are fine. Not as nice-looking as only/except, and the logic is OR for some reason, but whatever.</p>

<p>Next iteration: </p>

<pre><code>.terraform-plan-only:
  script:
    - cd "${TF_DIR}"
    - terraform init
    - terraform plan
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      changes:
        - $TF_DIR/**/*
</code></pre>

<p>Imagine ~100 commits where I mess with various ways to quote that variable.</p>

<p><a href="https://docs.gitlab.com/ee/ci/jobs/job_control.html#variables-in-ruleschanges">This link in the docs</a> shows something disconcertingly similar:</p>

<pre><code>docker build:  
  variables:
    DOCKERFILES_DIR: 'path/to/files'
  script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
  rules:
    - changes:
        - $DOCKERFILES_DIR/*
</code></pre>

<p>There are also threads like <a href="https://gitlab.com/gitlab-org/gitlab/-/issues/338312">this one</a> in the Gitlab repo showing that something like this is supposed to work.</p>

<p>But it doesn't work.</p>

<p>I'm guessing it doesn't work because it's an inherited configuration entry and because <code>rules:changes</code> seems to be a sort of special case for Gitlab CI. Part of which means that it can't use variables that are defined in a stage, but only more global variables. But who knows. I'm just going to repeat that segment for every different project, which is...fine. Just fine.</p>

<h5 id="tldr">TLDR;</h5>

<p>This works:</p>

<pre><code>variables:  
  MY_PROJECT: path/to/project

.terraform-plan-only:
  script:
    - cd "${TF_DIR}"
    - terraform init
    - terraform plan
  variables:
    TF_DIR: $MY_PROJECT
    WORKSPACE_NAME: my-project
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      changes:
        - $MY_PROJECT/**/*
</code></pre>

<p>but this <strong>doesn't</strong> work:</p>

<pre><code>variables:  
  MY_PROJECT: path/to/project

.terraform-plan-only:
  script:
    - cd "${TF_DIR}"
    - terraform init
    - terraform plan
  variables:
    TF_DIR: $MY_PROJECT
    WORKSPACE_NAME: my-project
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      changes:
        - $TF_DIR/**/*
</code></pre>

<p>I hope you ran across this when there's still some of your day to get back.</p>]]></content:encoded></item><item><title><![CDATA[Jest - mock a single function in a module]]></title><description><![CDATA[<p>Though the <a href="https://jestjs.io/docs/jest-object#jestrequireactualmodulename">requireActual</a> route looked promising, it only worked when I called the mocked function directly in my test, and not when used like in real life.</p>

<p><a href="https://stackoverflow.com/questions/45111198/how-to-mock-functions-in-the-same-module-using-jest">This answer</a> on StackOverflow ended up helping, as did <a href="https://github.com/facebook/jest/issues/936#issuecomment-545080082">this discussion</a> on GitHub.</p>

<p>It's definitely not beautiful. Almost makes me miss python mocks.</p>]]></description><link>http://104.236.78.148/2022/12/14/jest-mock-a-single-function-in-a-module/</link><guid isPermaLink="false">67ac46b4-d879-4345-87d2-e2b61a25a483</guid><dc:creator><![CDATA[Kyle Valade]]></dc:creator><pubDate>Wed, 14 Dec 2022 23:48:07 GMT</pubDate><content:encoded><![CDATA[<p>Though the <a href="https://jestjs.io/docs/jest-object#jestrequireactualmodulename">requireActual</a> route looked promising, it only worked when I called the mocked function directly in my test, and not when used like in real life.</p>

<p><a href="https://stackoverflow.com/questions/45111198/how-to-mock-functions-in-the-same-module-using-jest">This answer</a> on StackOverflow ended up helping, as did <a href="https://github.com/facebook/jest/issues/936#issuecomment-545080082">this discussion</a> on GitHub.</p>

<p>It's definitely not beautiful. Almost makes me miss python mocks.</p>

<p>Here's an example cribbed from that SO:</p>

<pre><code>// myFunctions.ts
import * as thisModule from './module';

export function world() {  
    return 'world';
}

export function hello() {  
    const value = world();
    return `hello ${thisModule.world()}`
}
</code></pre>

<pre><code>// myFunctions.spec.ts
import * as theModule from './myFunctions';

describe('function mock test', () =&gt; {  
  it('should change that function output', async () =&gt; {
    jest.spyOn(theModule, 'world').mockReturnValue('NOT WORLD!')
    ...
  });
});
</code></pre>]]></content:encoded></item><item><title><![CDATA[MLOps, CI, and development flows using Databricks]]></title><description><![CDATA[<p>I wrote this about a year or two ago while I was still working a lot with Databricks.  It was right around the time where "MLOps" and "ML Engineering" were starting to get kind of hip and posting then probably would have been a better idea 🤪. </p>

<p>I was very frustrated</p>]]></description><link>http://104.236.78.148/2021/02/04/mlops-development-flows-using-databricks/</link><guid isPermaLink="false">fedbbc2e-2677-433c-844c-74497de0c9fa</guid><dc:creator><![CDATA[Kyle Valade]]></dc:creator><pubDate>Thu, 04 Feb 2021 16:08:10 GMT</pubDate><content:encoded><![CDATA[<p>I wrote this about a year or two ago while I was still working a lot with Databricks.  It was right around the time where "MLOps" and "ML Engineering" were starting to get kind of hip and posting then probably would have been a better idea 🤪. </p>

<p>I was very frustrated by the machine learning workflow vs. what I am used to while developing web apps - it honestly felt like stepping back in time about 10-15 years. Like, you know, when you're learning to program and before you know about source control or automated testing. Except much slower because there is a ton of data that the ML training has to slog through. And the data science code that I've seen, while genius in its way, hasn't been written with maintainability in mind. Anyways, the good folks at Databricks pointed me to some resources and talked me through what they often saw, and I also rolled up my own sleeves. Hopefully this is still relevant for you.</p>

<hr>

<p>Working with notebooks is a lot different than your standard dev environment, and the notebooks themselves make it easy to get lazy and have a bunch of spaghetti code lying around. Plus, you have to figure out a whole different way to work, integrate with source control, integrate with others, and run your automated tests.</p>

<p>We’ve gone through a couple of iterations for organizing projects, and have talked to Databricks about how they structure their own. This is what we’ve come up with and what we’ve been using for our ML project. So far, we're pretty happy with it - it’s not much different than a lot of Git-based development workflows + it has scaled well so far beyond one developer + across multiple different environments.</p>

<p>This is intended as a framework for future machine learning projects. No doubt there is room for improvement, so feel free to experiment and report back.</p>

<h3 id="workflow">Workflow</h3>

<p>In terms of a workflow, I’m a fan of something similar to <a href="https://guides.github.com/introduction/flow/">GitHub Flow</a> (or there are a million variants).</p>

<p>The flow goes roughly like:</p>

<ul>
<li>Open a branch per change</li>
<li>Commit your code into that branch</li>
<li>Open a pull request in GitHub</li>
<li>Go through some code review</li>
<li>Make changes (or not)</li>
<li>Merge</li>
<li>Deploy</li>
<li>Repeat</li>
</ul>

<h3 id="structure">Structure</h3>

<p>Here is our ideal directory structure for the project.</p>

<pre><code>|ProjectName/  -- the project root
----| azure-pipelines.yml
----| requirements.txt
----| README.md
----| .gitignore
----| scripts/
----| tests/
----| forecasting
--------| __init__.py
--------| conf/
--------| ModelTraining.py
--------| set_environment.py
</code></pre>

<p><img src="https://lh6.googleusercontent.com/lAR8j4-pDzg1yNARe_QVjTuLIkF1VfH6Rixo1ELOeb6Wn2fexcqIKPgoj-8bp6QFGaPbP_-BMsML-kkDAfYQVVsxxyldCUn6JHfZKJxfJxnZqhq1Ik1T9_gwvaDkeJEJb5PvJ-Ei" alt="img"></p>

<p>A lot of these are explained in the Databricks section, so I’ll just go over the rest of them here:</p>

<ul>
<li><strong>azure-pipelines.yml</strong> - this is the config for our CI system (Azure Pipelines)</li>
<li><strong>requirements.txt</strong> is your standard python file for listing your project’s requirements. It’ll be used by your package manager.</li>
<li><strong>README.md</strong> - standard git repo readme. .md is a markdown file</li>
<li><strong>.gitignore</strong> - all of the files you don’t want to store in source control</li>
<li><strong>tests</strong> - the root for all of your tests. There are other places to put this, and it’s mostly a matter of preference. 
<ul><li>The way everything else is set up, if you put it under <code>forecasting</code>, then you'll send it up to Databricks all the time, so I kept it outside.</li></ul></li>
<li><strong>scripts</strong> - if you have any helpful scripts for your project (like deploying your code to Databricks (see script in appendix)), this is the place for those</li>
<li><strong>forecasting</strong> - the top level python module. It contains all of the notebooks. Name it something that will make sense when you're importing it in your code.</li>
</ul>

<p>There's some room for debate here...for this project, we kept the machine learning library separate from this code, since we think it'll have some broader use. But if you don't think that's the case, you might want to rename <code>forecasting</code> to be something like <code>forecasting_notebooks</code>, and then your machine learning library source would be a separate dir under root. </p>

<h2 id="databricks">Databricks</h2>

<h3 id="workflow">Workflow</h3>

<p>Databricks is great, but we’ve found it’s better to pull a lot of your code out into regular python packages and import them into your notebook. That’s better because:</p>

<ul>
<li>You can write + run automated tests locally</li>
<li>Refactoring is much easier in a real IDE</li>
<li>We had a lot of trouble pickling classes that were defined in notebooks - so you’ll <em>need</em> to do this for a lot of machine learning</li>
<li>Fiddling with the Databricks-git integration is a bit of a pain</li>
<li>It’s easier to treat your Databricks environment as disposable + see Git as the source of truth. 
<ul><li>Not sure if the Databricks code is out of date? Just overwrite it with your local copy</li></ul></li>
</ul>

<p>This is how we import our package at the top of our databricks notebook:</p>

<pre><code>dbutils.library.install("dbfs:/libraries/StructuralChangeDetectionModel-0.4811.2-py3.7.egg")  
dbutils.library.restartPython()  
</code></pre>

<h3 id="databricksdirectorystructure">Databricks directory structure</h3>

<p>Looking at the directory structure from within Databricks, it looks like this. It mostly mirrors the structure of your project on GitHub except for the autogenerated CI directory, and where your local code is written.</p>

<p><img src="https://lh3.googleusercontent.com/JXbbR7ZCayP2viHJyThWUb8BLiIHlC48S1H0POqkKq-A3QePZMfZGafK65emOzDJlQOkp54vfRLt-AIIhaGhO_sW7mZGS83_HVHUnQcF-5omAjEsI2Mt_3JNQifE9e4UMdzCnQUs" alt="img"></p>

<pre><code>|ProjectName/ - should be accessible by all people in the project - ie, not in your personal folder
----| master/ - the production copy of your project
--------| conf/
------------| production_config.py
------------| ci_config.py
------------| kyle_config.py
------------| joe_config.py
------------| ci_set_environment.py
--------| set_environment.py
--------| Model training notebook.py
----| CI/ - where your CI script will store all of its builds - these are autogenerated
--------| 49-master/  - the build number + the name of the branch being built, for example
--------| 37-BI-5613/ 
----| libraries/
----| BI-4810/ - not part of source control; feature/dev/bugfix branch
----| BI-5613/ - not part of source control; feature/dev/bugfix branch
</code></pre>

<p>To explain…</p>

<ul>
<li><strong>ProjectName</strong> - Your Databricks project root. Obvs name this something that makes sense for your project. 
<ul><li>Contains all of your project files - nothing for your project should be outside of this (in Databricks)</li></ul></li>
<li><strong>master</strong> - contains the project files that should be used for production. Mirrors the directory in GitHub where your code lives</li>
<li><strong>conf</strong>  - contains files that define the constants that you’ll be using for your project on a per-environment (and/or per-developer) basis. Things are split up so that it’s easy to switch between environments.
<ul><li>The files in here shouldn't contain a bunch of util functions or anything - those can be defined in a real notebook or in your library. This should pretty much just be constants - especially the ones that change between prod and dev</li></ul></li>
<li><strong>production<em>_config</em></strong> - the base config. I prefer when everything inherits from production because you can see exactly what changed and there’s not as much duplication. But other people like doing a base_config and then branching off from there. Either way, this will contain all of the constants you need for your prod environment.</li>
<li><strong>ci<em>_set</em>_environment</strong> - this is just used by the CI script - it sets the environment for the CI job</li>
<li><strong>ci<em>_config/kyle</em><em>_config/joe</em>_config</strong> - Environment-specific settings. Ie - the CI environment vs Kyle’s dev environment vs Joe’s dev environment. You can set the delta files to your own sandbox, instead of overwriting prod, etc.</li>
<li><strong>set_environment</strong> - this is a one-line notebook that specifies which conf file to use</li>
<li><strong>BI-4810 + BI-5613 + etc…</strong> - These aren't stored in source control. When you send your local code up to Databricks, it would write everything into these directories (see the script at the very bottom of this post). These are just examples of feature branch/bug fix folders - feel free to name them whatever you want. I personally like using the issue number.
<ul><li>It’s basically the same as master except some of the files will be different, because you’re adding a feature or fixing bugs.</li>
<li>Another option is to use separate folders for each of your developers (ie. a KyleDev + JoeDev folder). This is actually what Databricks suggested, but I don't really like the option because... 
<ul><li>I found that it doesn’t work super well if multiple people are working on the same issue. For instance, if you have a data engineer + a data scientist. </li>
<li>Having separate folders per branch mirrors GitHub flow closer, which is nice. </li>
<li>On long-lived branches, you’re more likely to diverge from master and fall into bad habits.</li></ul></li></ul></li>
<li><strong>CI</strong> - the folders in here will be automatically created by your CI script, if you have that set up. It comes in handy if you’re trying to fix an issue that caused integration tests to fail.</li>
<li><strong>libraries</strong> - contains dependencies that need to be added to your cluster - should contain the versions used for master. 
<ul><li>If there are any changes - if you’re upgrading one of the libraries and it’s troublesome, for instance - then add a libraries subdirectory to your feature branch folder so that you don’t crash production.</li></ul></li>
</ul>

<h4 id="set_environment">set_environment</h4>

<p>Here it is in its entirety</p>

<p><code>%run ./conf/ci_config</code></p>

<p>And replace <code>ci_config</code> with whichever config you want to be using.</p>

<h4 id="set_environmentvsconf">set_environment vs conf</h4>

<p>The reason that we need a <code>set_environment</code> file is so that it's easy to switch between environments (prod and dev) without having to edit a real notebook. It decouples the environment from the code.</p>

<p>For example, what if we didn't have a <code>set_environment</code> file, and just specified whichever config file at the top of our Model Training notebook? Then everytime we were making a change, we'd have to remember to change our conf file reference first, and then change it back to prod before we commit the file to source control. And chances are that we would eventually use our prod settings in dev, or our dev settings in prod. Meaning that we could overwrite an important delta file, or not realize that our "prod" run has actually just been a "dev" run for a few weeks.</p>

<p>Or how would you dynamically set the environment? With CI, for example. You'd have to do some string parsing and remove the 3rd command from the top of the notebook or whatever, and it would be kind of messy and brittle. With the <code>set_environment</code> file, CI just needs to dump a single line into a new file for everything to work.</p>

<h4 id="secrets">Secrets</h4>

<p>Secrets (any sort of key or password) should be stored in <a href="https://docs.azuredatabricks.net/user-guide/secrets/secret-scopes.html#create-an-azure-key-vault-backed-secret-scope">Azure Key Vault</a> or <a href="https://docs.azuredatabricks.net/user-guide/secrets/secret-scopes.html#create-a-databricks-backed-secret-scope">Databricks secrets</a>. </p>

<p>Secrets should never be put directly in a Databricks notebook unless you clear the revision history afterwards. Otherwise, anyone with access to the notebook will be able to go back and find them.</p>

<h3 id="securityconnectingtodatasources">Security! Connecting to data sources</h3>

<p>One important point is that we don't want to mount the data lake as a drive due to security concerns. If we do that, then anyone with access to the Databricks instance can see everything in the data lake. By providing the oauth tokens, we can control access better.</p>

<p>See the <a href="https://docs.databricks.com/spark/latest/data-sources/azure/azure-datalake.html">Databricks docs</a> for more info.</p>

<h2 id="continuousintegrationci">Continuous Integration (CI)</h2>

<p>This is still fairly new, but has actually saved a whole bunch of time already. Also, it really takes the mental weight off when you think about running a deployable version of your code.</p>

<p>The idea is that every time you push to GitHub, the job will run the model training notebook. That notebook runs on only a small subset of the data so that it goes quickly, meaning that you'll have to make your data source a little dynamic (ie - put it in your environment conf). The notebook can have some <code>assert</code> statements to make sure that things pass some baseline expectation. And then CI will tell you whether it passed or failed. </p>

<p>So it’s on you to define any asserts or tests inside of your notebooks, but otherwise, if an exception happens for another reason, it will fail and it will show that in GitHub.</p>

<p>So you’ll want to set something up in Azure Pipelines and configure your azure-pipelines.yml file (see the Git project structure)</p>

<pre><code class="language-python">import base64  
import os  
import time

from databricks_api import DatabricksAPI  
from databricks_cli.workspace.api import WorkspaceApi  
from databricks_cli.sdk.api_client import ApiClient


BASE_PATH = os.path.dirname(os.path.dirname(__file__))

# Pre-defined Pipelines variables
# See https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&amp;tabs=yaml
PIPELINES_BUILD_ID = os.environ['PIPELINES_BUILD_ID']  
GIT_BRANCH_NAME = os.environ['GIT_BRANCH']

NOTEBOOK_DIR = '/Projects/ProjectName/CI/{}-{}'.format(  
    PIPELINES_BUILD_ID, GIT_BRANCH_NAME)

api_key = os.environ['DATABRICKS_API_KEY']  
headers = {  
    'Authorization': 'Bearer {}'.format(api_key)
}

databricks = DatabricksAPI(  
    host='https://eastus2.azuredatabricks.net',
    token=api_key
)


def send_code_to_workspace():  
    client = ApiClient(
        host='https://eastus2.azuredatabricks.net',
        token=api_key
    )
    workspace_api = WorkspaceApi(client)
    workspace_api.import_workspace_dir(
        source_path=BASE_PATH,
        target_path=NOTEBOOK_DIR,
        overwrite=True,
        exclude_hidden_files=True
    )


def send_config_to_workspace():  
    env_file_path = os.path.join(BASE_PATH, 'conf', 'ci_set_environment.py')
    set_env_file_contents = open(env_file_path, 'rb').read()
    set_env_base64 = base64.b64encode(set_env_file_contents).decode('ascii')

    # Create set_environment file
    databricks.workspace.import_workspace(
        path='{}/set_environment'.format(NOTEBOOK_DIR),
        content=set_env_base64,
        language='PYTHON',
        overwrite=True
    )


def wait_for_complete(run_id):  
    stopped_states = ['TERMINATED', 'SKIPPED', 'INTERNAL_ERROR']

    sleep_time_seconds = 60 * 2
    run_info = {}

    status = None
    while status is None or status not in stopped_states:
        if status is not None:
            time.sleep(sleep_time_seconds)

        run_info = databricks.jobs.get_run(run_id=run_id)
        status = run_info['state']['life_cycle_state']

    return run_info


def execute_on_databricks():  
    send_code_to_workspace()
    send_config_to_workspace()

    job_name = 'CI-{}-{}'.format(PIPELINES_BUILD_ID, GIT_BRANCH_NAME)
    cluster_info = {
        'spark_version': '5.3.x-scala2.11',
        'autoscale': {
            'min_workers': 2,
            'max_workers': 13
        },
        'custom_tags': {
            'cluster-purpose': 'CI-testing',
        },
        'node_type_id': 'Standard_F8s_v2',
        'driver_node_type_id': 'Standard_F8s_v2',
        'spark_env_vars': {
            "PYSPARK_PYTHON": "/databricks/python3/bin/python3",
        }
    }

    job_response = databricks.jobs.submit_run(
        run_name=job_name,
        new_cluster=cluster_info,
        libraries=[
            {'pypi': {'package': 'pandas'}},
            {'pypi': {'package': 'sentry-sdk'}},
        ],
        notebook_task={
            'notebook_path': '{}/ModelTraining'.format(NOTEBOOK_DIR),
        }
    )

    run_id = job_response['run_id']

    run_info = wait_for_complete(run_id)
    result_state = run_info['state']['result_state']
    if result_state != 'SUCCESS':
        print(run_info)
        raise Exception("Databricks run not successful - status {}".format(result_state))

    print('job id', run_id)


print(GIT_BRANCH_NAME, PIPELINES_BUILD_ID)

execute_on_databricks()  
</code></pre>

<p>What that example script does is basically orchestrate sending the right version of code to Databricks + creating a cluster + executing the pipeline along with the assert tests + waiting for results.</p>

<p>In order for this to work, you’ll need to have a subset of data available in your Databricks CI conf settings, because the job needs to take like 25 minutes or less, or it will just fail. You want the fast feedback at this point, anyways. Eventually we’ll write something that can do a more complete run on the data.</p>

<h2 id="appendix">Appendix</h2>

<h3 id="helpfulreads">Helpful reads</h3>

<ul>
<li><a href="https://thedataguy.blog/ci-cd-with-databricks-and-azure-devops/">CI/CD with Databricks and Azure Devops</a></li>
<li><a href="https://databricks.com/blog/2017/10/30/continuous-integration-continuous-delivery-databricks.html">CI/CD with Databricks</a></li>
</ul>

<h3 id="code">Code</h3>

<h4 id="azurepipelinesyml">Azure-pipelines.yml</h4>

<p>This is what we use for our project</p>

<pre><code># Python package
# Create and test a Python package on multiple Python versions.
# Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/python

pr:  
  autoCancel: true
  paths:
    exclude:
    - README.md

jobs:

- job: 'Test'
  pool:
    vmImage: 'Ubuntu-16.04'
  strategy:
    matrix:
      Python36:
        python.version: '3.6'
    maxParallel: 4

  steps:
  - task: UsePythonVersion@0
    inputs:
      versionSpec: '$(python.version)'
      architecture: 'x64'

  - script: python -m pip install --upgrade pip &amp;&amp; pip install -r requirements.txt
    displayName: 'Install dependencies'

  - script: python -m unittest
    displayName: 'unittest'

  - script: python ./forecasting/ci/run_in_databricks.py
    displayName: 'Run in Databricks'
    env:
      GIT_BRANCH: $(Build.SourceBranchName)
      PIPELINES_BUILD_ID: $(Build.BuildId)
      DATABRICKS_API_KEY: $(DATABRICKS_API_KEY)

  - task: PublishTestResults@2
    inputs:
      testResultsFiles: '**/test-results.xml'
      testRunTitle: 'Python $(python.version)'
    condition: succeededOrFailed()
</code></pre>

<h4 id="sendyournotebookstodatabricks">Send your notebooks to Databricks</h4>

<p>Run this file like <code>./send-to-databricks.ps1 BI-4811</code>. It was written for Windows, but a shell script should be similar.</p>

<pre><code># Send your current code to a dir in databricks. It will overwrite any conflicting files.


$SCRIPTPATH=$PSScriptRoot
$PROJECT_ROOT = (get-item $SCRIPTPATH).parent.FullName
$DATABRICKS_BASE_DIR="/Projects/ProjectName"
$BRANCH_NAME=$Args[0]
$DATABRICKS_DEST_DIR="$DATABRICKS_BASE_DIR/$BRANCH_NAME"
$LOCAL_CODE_DIR="$PROJECT_ROOT/forecasting/"

if ($BRANCH_NAME -eq $Null) {  
    throw "No git branch/directory name"
}

echo "checking out $BRANCH_NAME into Databricks $DATABRICKS_DEST_DIR"

databricks workspace import_dir --exclude-hidden-files --overwrite $LOCAL_CODE_DIR $DATABRICKS_DEST_DIR  
databricks workspace rm $DATABRICKS_DEST_DIR/set_environment

echo "Complete"  
</code></pre>

<h4 id="deployyourlibrarycodetodatabricks">Deploy your library code to Databricks</h4>

<p>You'll need to set up databricks-cli for this bash script to work. And be mindful, cuz this will overwrite everything.</p>

<pre><code class="language-sh">SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )"

. $SCRIPTPATH/env/Scripts/activate
python setup.py bdist_egg  
databricks --profile MYPROFILE fs cp -r --overwrite ./dist dbfs:/libraries  
deactivate  
</code></pre>]]></content:encoded></item><item><title><![CDATA[In Soviet Russia, ad finds you]]></title><description><![CDATA[<p>This is a post I wrote for work to explain the basics and foundations of ad tracking. It was meant for a fairly nontechnical audience, so there are probably some oversimplifications. There are also possibly some mistakes because I'm just looking at ads from the outside - I've never really</p>]]></description><link>http://104.236.78.148/2020/02/28/in-soviet-russia-ad-finds-you/</link><guid isPermaLink="false">c4254480-68e6-4189-9648-ad13598a23c2</guid><dc:creator><![CDATA[Kyle Valade]]></dc:creator><pubDate>Fri, 28 Feb 2020 13:10:11 GMT</pubDate><media:content url="http://104.236.78.148/content/images/2020/02/Chinatown-032.jpg" medium="image"/><content:encoded><![CDATA[<img src="http://104.236.78.148/content/images/2020/02/Chinatown-032.jpg" alt="In Soviet Russia, ad finds you"><p>This is a post I wrote for work to explain the basics and foundations of ad tracking. It was meant for a fairly nontechnical audience, so there are probably some oversimplifications. There are also possibly some mistakes because I'm just looking at ads from the outside - I've never really dealt with them professionally. Just, like, every day on the internet.</p>

<hr>

<p>So there you are, hanging out, talking about your favorite cleaning product. Next thing you know, you see an ad for Tide on Instagram. What happened? Is your phone listening to you? Or is there something deeper and more complex going on behind everything? Hopefully this article will shed some light, though maybe not on the conspiracy you thought. I'll try to keep it not very technical.</p>

<p><img src="http://104.236.78.148/content/images/2020/02/nosy-fellows.jpg" alt="In Soviet Russia, ad finds you"></p>

<h3 id="cookies">Cookies</h3>

<p>BUT FIRST: cookies! You need to know about cookies. Remember back in the 90's when you thought they sounded cute? They're the source of everything, but they’re just tiny bits of text that are attached to a website - little files, really. They're pretty helpful - they keep us logged in, they save things that we add to a cart, they save our Dark Mode preferences, and our darkest secrets.</p>

<p>You can see them if you open up your Chrome dev tools and go to the Application Tab like below. This is just a sample of the cookies we load on carhartt.com.</p>

<p><img src="http://104.236.78.148/content/images/2020/02/cookies.png" alt="In Soviet Russia, ad finds you"></p>

<p>Looks pretty boring, right? Well, look at the Domain column there...see anything funny? Facebook? Yahoo? doubleclick.net? channeladvisor.com? Those aren't Carhartt at all. They are, in fact, <strong>third-party cookies</strong>. How did they get added to carhartt.com?</p>

<h3 id="httprequests">HTTP requests!</h3>

<p>Sorry, did I say this wouldn't get technical? You also need to know about HTTP requests. You simply must. HTTP is what the internet is built on. It's how you view webpages and look at good dogs all day. Whenever you go to a website, your browser asks that website for the page's code (that's called an HTTP request), and the web server has a record of all of the requests. That record can look something like this:</p>

<p><img src="http://104.236.78.148/content/images/2020/02/access-log.png" alt="In Soviet Russia, ad finds you"></p>

<p>It's also important to note that everything else that a website pulls in goes through the same process. When you think of everything else that might be pulled in as part of a website, think: images, videos, code to make the page fancy and good-looking, code for analytics, etc. These are called "resources" in the biz. So up there, you might see requests for images (there's a line for GET /apache_pb.gif).</p>

<p>One of the cool parts of the internet is that people are able to kind of borrow other people's images. When they do that, a request is sent out to the site that "owns" the image ("hosts" would be the technical word). When you load a page where a picture is being pulled in like that, what you don't see is that the "owner" of the picture gets a tiny bit of information about you. For instance, if you're on carhartt.com, and we're loading an image from Scene7, then Scene7 will know exactly where that image is going. Or if I'm clicking on a page from Google, the site will know that, too. It's called the "Referer" [sic]. Along with that information, there are also some cookies that are being passed back and forth.</p>

<p>Here's what Chrome is sending to a website after I google "What are my http headers" and click on one of the links:</p>

<p><img src="http://104.236.78.148/content/images/2020/02/http-headers.png" alt="In Soviet Russia, ad finds you"></p>

<p>The referer is telling that website that I came from Google. The User-Agent is also interesting. You can check it out yourself <a href="https://www.whatismybrowser.com/detect/what-http-headers-is-my-browser-sending">here</a>. Note that these headers are easily spoofed, so it's not necessarily the best way of collecting information.</p>

<h3 id="bringingitalltogether">Bringing it all together</h3>

<p>Probably not super exciting so far, but this is where everything finally starts to tie together. Cookies and HTTP are important to know about because most websites load things from all over, which is why privacy has become such a big concern. Remember that sample of cookies I showed above? Carhartt.com is loading things from all of those sites and more. And when you load a resource, any cookies for that site are sent along with the rest of the information.</p>

<p>Let’s see how that applies to Facebook and Google. Not that they are the only examples - adtech is an area with a whole lot of different businesses involved. But they are two that everyone knows, and that probably have the most complex operations.</p>

<h4 id="facebook">Facebook</h4>

<p>Let's say <a href="https://www.pewresearch.org/fact-tank/2019/05/16/facts-about-americans-and-facebook/">69% of Americans have a Facebook account</a>. Let's say you logged into Facebook last night and were cruising your news feed. Imagine that "like" buttons are still popular (they don't need "like" buttons to get this info anymore). And also imagine that Carhartt has a "like" button on our product pages.</p>

<p>That "like" button is really just a snippet of code that Facebook tells the website people to copy-paste into the page source. In that code, there is an image that's loaded from facebook.com. Say you're looking at our classic Chore Coat, and that there is a "like" button underneath. Just by going to that page, Facebook knows that you're considering purchasing that chore coat. And they know that it might be ill-fitting what with your philosophy and computer science degree and that she never did love you after all...just for example... </p>

<p>How do they know all that? Well - remember cookies? You've been to Facebook.com, so there is a cookie set for that site. The cookie is essentially the key to your profile - it’s how you can close your tab and then come back to it later without having to sign in again. Facebook.com has a lot of your personal information -  like a list of your <a href="https://open.spotify.com/track/55gISxV37mffOW2DbSskT3">fav</a> <a href="https://open.spotify.com/track/0XKWxKy4DqjlvWGiNJoIKO">post</a> <a href="https://open.spotify.com/track/7GDURAuWFIfwK3lXzPRRFp">rock</a> <a href="https://open.spotify.com/track/1HfJV18PHF2UQqh4TuySBJ">bands</a> from 2005. You are loading their image, so you are sending them the referer (the Carhartt Chore coat page) and your profile (via the cookie). Now Facebook has a network of sites that you've visited and products that you've viewed and can tie that right to your identity.</p>

<p>Now imagine that you also have a mobile device. Imagine it's smart and that you visit websites with that device. Including Facebook. Now Facebook can tie different devices to your profile and can probably infer which are for work. And they definitely know your location (google <a href="http://letmegooglethat.com/?q=device+graphing">device graphing</a>).</p>

<p>Now imagine that you have friends. Imagine that those friends, too, have mobile devices. Imagine that they also visit Facebook and give them their location without necessarily realizing it. Imagine that you hang out with those friends. Well, Facebook already knew that - they have your friend network and everyone's locations.</p>

<h4 id="googleotheradtechcompanies">Google + other adtech companies</h4>

<p>My guess is that all ad networks would kind of work the same, but with less data than Google or Facebook (so they can argue that their ads are better and sell them for more $$).</p>

<p>When a website joins an ad network, let's say Google's specifically, they tell you to put a script in your site. A script can run any code it wants once it's loaded, so it's hard to say exactly what is in that script. But at a minimum, you'll be sharing the same information you're giving Facebook through those "like" buttons. Google is able to build up a network of sites that you've visited, so they know what to show you, or can guess, or just show something generic or profitable. They also know what you click on, and probably what your mouse hovers over, or if you've stopped scrolling so you can read the ad.</p>

<p>Not to get conspiratorial, but...</p>

<p>Remember Google+? My guess would be that they created that social network so they could tie ads directly to your profile. And it kind of worked. I'm always signed into Google when I'm searching, so they can tie everything right back to me.</p>

<p>Remember Chrome? Boy is it a popular browser. And they made it super easy to stay signed into Google and sync between devices.</p>

<p><a href="https://www.theverge.com/2018/8/13/17684660/google-turn-off-location-history-data">Do you ever use Google Maps or Waze?</a></p>

<p><a href="https://www.theverge.com/2017/11/21/16684818/google-location-tracking-cell-tower-data-android-os-firebase-privacy">Have an Android phone?</a></p>

<p>Anyways.</p>

<h4 id="beyondcookies">Beyond cookies</h4>

<p>I mentioned above that Facebook no longer needs “Like” buttons to track your browsing habits. Now that Facebook has proved itself to be a very valuable advertising tool, people will happily let them take whatever information they want (see: <a href="https://developers.facebook.com/docs/facebook-pixel/implementation/">Facebook pixel</a>). Facebook isn’t the only company that does this, of course, they are just a stand-in for any other company that likes data. Google and Amazon definitely do it. I’m sure Adobe does. Even <a href="https://www.youtube.com/watch?v=2RPerSEvP4Y">educated fleas</a> do it. Any third-party script that someone adds to their page can take whatever information they want from the user. </p>

<p>What is a script? A script in this context is a chunk of code that people add to their site, and it runs whenever the page is loaded. Take Google Analytics - they give you a snippet of code, which in turn loads some of their code, and gives you the user journeys on your site. There are even some websites that use their users to mine bitcoin (not fantastic for battery life). That code is often obfuscated (“minified”), so again, it’s hard to say exactly what each script does. You mostly have to trust the organization whose code you’re pulling in.</p>

<p>Now that all of these websites have scripts from Facebook and Google added willy nilly, and since those are two of the major online advertisers (and gatekeepers for your online identity), there is less of a need for third-party cookies. Which is convenient, because the world is actually moving away from third-party cookies as a means of tracking (Mozilla and Safari block them by default + Chrome will be doing it soon).</p>

<p>I don't really want to get into mobile apps here, but assume that they are, in some ways, even more permissive than scripts. Apple won't let apps do any sneaky bitcoin mining, but the apps are often given access to your location, photos, contacts, etc.</p>

<p><img src="http://104.236.78.148/content/images/2020/02/its-chinatown.jpg" alt="In Soviet Russia, ad finds you"></p>

<h3 id="targetedadvertising">Targeted advertising</h3>

<p>The companies have all of your data - now let’s look at what they do with it:</p>

<p><img src="http://104.236.78.148/content/images/2020/02/pasted-image-0.png" alt="In Soviet Russia, ad finds you"></p>

<p>Do you see that, or have you unconsciously blocked it?</p>

<p>For all of the data they collect, what you see is still just some banner ad. Maybe it’s for a brand that’s a bit more relevant. There’s that cliche: “The greatest minds of our time are thinking about how to make people click ads”, which...yeah, probably. The relatively new Amazon ad team started bringing in $1 billion in like a year. That’s because with all of the knowledge that they have of consumers, these companies can charge a lot of money - <a href="https://techcrunch.com/2019/01/20/dont-be-creepy/">up to 500% more</a> - for targeted ads vs. untargeted ads. That is, ads that show you something tailored to your personal profile vs. ads that show you something less bespoke. That is, ads that track you vs. ads that don’t. There <a href="https://techcrunch.com/2019/05/31/targeted-ads-offer-little-extra-value-for-online-publishers-study-suggests/">are</a> <a href="https://techcrunch.com/2019/01/20/dont-be-creepy/">reports</a> that targeted advertising isn’t worth the extra money, so we’ll see how that shakes out, but people generally seem to think they are. Contextual ads are one of the alternatives.</p>

<p>There are other applications of your data, too, like training facial recognition algorithms based off of all of the labelled pictures that have been posted to Facebook and Instagram or creating recommendation engines (think Netflix or Amazon's suggestions).</p>

<h3 id="privacy">Privacy</h3>

<p>The reason that a lot of sites join ad networks or bring in third-party scripts from adtech companies is simple - they want to make money. They want each person that reads their blog to give them a fraction of a penny so maybe they can dream of paying their bills with the proceeds.</p>

<p>When businesses do it, it’s typically because they want to sell more of their product. Large parts of Facebook, etc. are dedicated to proving to their advertisers that the ad spend of those companies is generating large returns (how honest they are is <a href="https://thecorrespondent.com/100/the-new-dot-com-bubble-is-here-its-called-online-advertising">up for debate</a>). So companies put the scripts on their site.</p>

<p>User privacy usually isn’t part of the equation, or if it is, well, they have to drive traffic, convert users, and sell things. I mean, imagine if a company didn’t do that - it would be crazy - just completely irresponsible. Who knows - they might go out of business because of it. And, what, big tech knows that the user is looking at well-made, hardworking apparel? That’s hardly the end of the world.</p>

<p>And, as a web developer myself, that stuff is hard to avoid adding. You need to know your traffic and the users on your site, and there isn’t time to build something new. So if Google has a tool I can add that’s free or cheap, and that has been tested by essentially the entire internet, I’m probably going to go with that. Repeat that process for other foundational tools.</p>

<p>The problem is that the number of sites with those tools add up, creating a huge network, with the effect that big tech knows about <a href="https://www.theverge.com/2014/4/4/5581884/how-advertising-cookies-let-observers-follow-you-across-the-web">90% of the sites</a> you visit, and can track you everywhere you go (via phone location data). How much of what you think about is reflected in search terms or online research? Messages to friends? Photos, and online posts? That's a whole lot of your physical, mental, and emotional state that is being tracked by these companies. And oftentimes sold, directly or indirectly.</p>

<p>How can you guard against that? <a href="https://www.theverge.com/2018/6/7/17434522/online-privacy-tools-guide-chrome-windows">Here is a guide</a>. Or, off the top of my head, you can block third-party cookies, for one. Turn off location sharing in your apps + exit them when you aren’t using them. And/or opt for their websites instead. Firefox also has better privacy defaults than Chrome. Delete Facebook? Or you can use Tor and never sign into social networks. Eh - better just use that guide.</p>

<p>There are also some interesting ideas where you generate thousands of online profiles with your name and a bunch of different interests so that the real you is obfuscated.</p>

<h3 id="conclusion">Conclusion</h3>

<p>So that's how, once you go to a company's website, you start seeing their logo on top of half the internet. It began with cookies and HTTP and has since evolved. I hope that answered all of your- what's that? The Instagram ad for Tide? Oh, no, Facebook was definitely listening to you.</p>

<h4 id="etc">Etc.</h4>

<ul>
<li>See this <a href="https://gimletmedia.com/shows/reply-all/z3hlwr">Reply All</a> episode - "Is Facebook Spying on You?"</li>
<li>The Verge - <a href="https://www.theverge.com/2018/6/7/17434522/online-privacy-tools-guide-chrome-windows">Guide to protecting your data</a></li>
<li><a href="https://blog.mozilla.org/firefox/online-advertising-strategies">Mozilla article</a> about online tracking strategies and how to protect yourself</li>
<li><a href="https://vicki.substack.com/p/one-very-bad-apple">Great article</a> from Normcore Tech about where Apple's pro-privacy stance hits reality</li>
<li>The Verge - <a href="https://www.theverge.com/2018/6/7/17434522/online-privacy-tools-guide-chrome-windows">"How to increase your privacy online"</a></li>
<li>The Verge - <a href="https://www.theverge.com/2019/3/5/18252397/facebook-android-apps-sending-data-user-privacy-developer-tools-violation">“Some major Android apps are still sending data directly to Facebook: Even when you’re not logged in or don’t have a Facebook account”</a></li>
<li>The Verge - <a href="https://www.theverge.com/2014/4/4/5581884/how-advertising-cookies-let-observers-follow-you-across-the-web">"How advertising cookies let observers follow you across the web"</a></li>
<li><a href="https://en.wikipedia.org/wiki/HTTP_referer">HTTP referer</a></li>
</ul>

<p>Yeah, pretty heavy on The Verge...</p>]]></content:encoded></item><item><title><![CDATA[ODBC and DB2 - problem saving a large chunk of text - CWBNL0107]]></title><description><![CDATA[<p>I'm using PyODBC to connect to DB2 and was seeing some problems saving certain rows of data. The column the error message mentioned was a CLOB type, and the error happened when the column had data larger than, say 10,000 characters. The error message reads thusly:</p>

<pre><code>(pyodbc.DataError) ('22018',</code></pre>]]></description><link>http://104.236.78.148/2020/01/30/odbc-and-db2-problem-saving-a-large-chunk-of-text-cwbnl0107/</link><guid isPermaLink="false">ea2e4dff-4d92-47da-90fb-dd7e7d3264ae</guid><dc:creator><![CDATA[Kyle Valade]]></dc:creator><pubDate>Thu, 30 Jan 2020 15:17:07 GMT</pubDate><content:encoded><![CDATA[<p>I'm using PyODBC to connect to DB2 and was seeing some problems saving certain rows of data. The column the error message mentioned was a CLOB type, and the error happened when the column had data larger than, say 10,000 characters. The error message reads thusly:</p>

<pre><code>(pyodbc.DataError) ('22018', '[22018] [IBM][System i Access ODBC Driver]Column 6: CWBNL0107 - Converted 9739 bytes, 4869 errors found beginning at offset 0 (scp=1202 tcp=37 siso=1 pad=0 sl=9739 tl=19478) (30200) (SQLPutData); [22018] [IBM][System i Access ODBC Driver]Error in assignment. (30019)')
</code></pre>

<p>Googling the error, some people had mentioned the charset, so I tried tweaking that a little bit. Didn't work. Plus, some records were being saved, so it didn't make sense.</p>

<p>Then <a href="https://www.ibm.com/support/pages/client-access-odbc-driver-truncates-character-fields-contain-null">IBM themselves</a> recommended turning on the Allow unsupported character option (in this case, through the <code>AllowUnsupportedChar</code>/<code>ALLOWUNSCHAR</code> arg in the connection string, though that part isn't really documented, like pretty much everything related to db2). I tried that, and I didn't get an error. But it mangled like half the data in a large row. </p>

<p>Parentheticals aside - not a good solution. I prefer the error, thank you very much. And I still suspected that the problem had to do with the size of the data.</p>

<p>Then I found <a href="http://www.sqlthing.com/HowardsODBCiSeriesFAQ.htm">this site</a> which seems to be documenting all of the db2 connection string args, and it is a magical wonderland. I did a ctrl + f and searched for "length" and boom:</p>

<blockquote>
  <p>The MAXFIELDLEN keyword, (can also be specified as MaxFieldLength), controls how much LOB (large object) data is sent in a result set. The value indicates the size threshold in kilobytes and the default value is 15360 and in V5R2 the maximum value allowed is 2097152, (2MB). If a LOB is larger than this value, you will have to use subsequent calls to retrieve the rest of the LOB data in your application.</p>
</blockquote>

<p>Now I think there's something wrong with the math there, because 2097152KB certainly isn't 2MB. The default was also far from 15360KB in my system. Anyways, I set <code>MAXFIELDLEN=2056</code> in my connection string (<a href="http://nightlyclosures.com/2020/01/22/connect-to-db2-from-python-with-sqlalchemy/">see here</a> for my post about the connection string) and everything worked like magico. I hope it works like magico for you, too.</p>]]></content:encoded></item><item><title><![CDATA[Connect to DB2 from python with SQLAlchemy]]></title><description><![CDATA[<p>This is kind of a sister post to my <a href="http://nightlyclosures.com/2020/01/16/access-db2-from-databricks/">Databricks-specific post</a> from the other day.</p>

<p>It's amazing how much time you can spend searching through docs, fiddling with connection strings, and trying different engines because some <code>&lt;cough&gt;</code> IBM <code>&lt;/cough&gt;</code> don't seem to be working very well</p>]]></description><link>http://104.236.78.148/2020/01/22/connect-to-db2-from-python-with-sqlalchemy/</link><guid isPermaLink="false">dddd2f3b-0d9e-46a1-9726-a3b2d898430b</guid><dc:creator><![CDATA[Kyle Valade]]></dc:creator><pubDate>Wed, 22 Jan 2020 14:45:19 GMT</pubDate><content:encoded><![CDATA[<p>This is kind of a sister post to my <a href="http://nightlyclosures.com/2020/01/16/access-db2-from-databricks/">Databricks-specific post</a> from the other day.</p>

<p>It's amazing how much time you can spend searching through docs, fiddling with connection strings, and trying different engines because some <code>&lt;cough&gt;</code> IBM <code>&lt;/cough&gt;</code> don't seem to be working very well or the docs aren't quite up to date or whatever. </p>

<p>Maybe there are other people that need to use DB2 for whatever godawful reason. Maybe those people want to start using Python or Airflow or something. Maybe those people are just me six months from now. Here is what got everything working.</p>

<p>TLDR; Use pyodbc with <code>ibm_db_sa</code>. The connection string should look like</p>

<p><code>'ibm_db_sa+pyodbc400://{username}:{password}@{host}:{port}/{database};currentSchema={schema}'</code></p>

<p>Now for the long form answer...</p>

<pre><code># requirements.txt

ibm_db  
ibm_db_sa  
pyodbc  
SQLAlchemy  
</code></pre>

<pre><code class="language-python"># database_engine.py

from contextlib import contextmanager

from sqlalchemy import create_engine  
from sqlalchemy.orm import sessionmaker


def create_database_engine():  
    connection_string = 'ibm_db_sa+pyodbc400://{username}:{password}@{host}:{port}/{database};currentSchema={schema}'.format(
        username='',
        password='',
        host='',
        port='',
        database='',
        schema=''
    )
    return create_engine(connection_string)


engine = create_database_engine()


def create_session():  
    Session = sessionmaker(bind=engine)
    return Session()


@contextmanager
def session_scope():  
    """Provide a transactional scope around a series of operations."""
    session = create_session()
    try:
        yield session
        session.commit()
    except Exception:
        session.rollback()
        raise
    finally:
        session.close()
</code></pre>

<p>Here is the model:  </p>

<pre><code class="language-python"># models.py

from sqlalchemy import Column, Integer, String, DateTime, Text, MetaData  
from sqlalchemy.ext.declarative import declarative_base

from database_engine import engine


metadata = MetaData(schema='Restaurant')  
Base = declarative_base(bind=engine, metadata=metadata)


class Transaction(Base):  
    __tablename__ = 'Transaction'

    id = Column('ID', String(100), primary_key=True)
    store_id = Column('STORE_ID', Integer)
    created_time = Column('CREATED_TIME', DateTime)
    transaction_json = Column('TXN_JSON', Text)
</code></pre>

<p>And a sample use:</p>

<pre><code class="language-python">from database_engine import session_scope  
from models import Transaction


if __name__ == '__main__':  
    with session_scope() as session:
        results = session.query(Transaction).limit(10)
        for result in results:
            print(result.id)
</code></pre>]]></content:encoded></item><item><title><![CDATA[Access DB2 From Databricks]]></title><description><![CDATA[<p>This took me a good few hours to figure out. So hopefully it will help you and my future self.</p>

<ul>
<li>install <code>com.ibm.db2.jcc:db2jcc:db2jcc4</code> on your cluster from maven</li>
<li>Get your license file dir (this is a whole process in itself)</li>
<li>From your license info, copy the</li></ul>]]></description><link>http://104.236.78.148/2020/01/16/access-db2-from-databricks/</link><guid isPermaLink="false">f4856d04-b14b-4df7-9445-a18556f1d01e</guid><dc:creator><![CDATA[Kyle Valade]]></dc:creator><pubDate>Thu, 16 Jan 2020 20:55:14 GMT</pubDate><content:encoded><![CDATA[<p>This took me a good few hours to figure out. So hopefully it will help you and my future self.</p>

<ul>
<li>install <code>com.ibm.db2.jcc:db2jcc:db2jcc4</code> on your cluster from maven</li>
<li>Get your license file dir (this is a whole process in itself)</li>
<li>From your license info, copy the jar file (mine is like <code>db2jcc*.jar</code>) up to databricks using databricks-cli. 
<ul><li>I copied them to a tmp dir and then moved them to <code>/dbfs/FileStore/jars/maven/com/ibm/db2/jcc/license</code> from a notebook, but that might not be necessary</li>
<li>You might also have to copy the <code>.lic</code> files into the same dir, but, again, I haven't validated that.</li></ul></li>
<li>install that jar on your cluster as a library</li>
<li>restart your cluster</li>
</ul>

<p>Then you can run this (python) code:</p>

<pre><code>connection_string = 'jdbc:db2://{host}:{port}/{database}:currentSchema={schema};database={database};user={username};password={password};'.format(  
    host=host, 
    port=port,
    schema=default_schema,
    database=database, 
    username=username, 
    password=password
)

rdd = spark.read.format("jdbc") \  
    .option('url', connection_string) \
    .option('driver', 'com.ibm.db2.jcc.DB2Driver') \
    .option('dbtable', 'my_table') \
    .load()

display(rdd)  
</code></pre>

<p>Hurrah!</p>]]></content:encoded></item><item><title><![CDATA[7x performance improvement for my dead slow SQL Server + Django pyodbc queries]]></title><description><![CDATA[<p>In <a href="http://104.236.78.148/2019/10/16/working-with-unmanaged-sql-server-in-django-pt-ii/">one of my Django projects</a>, I'm connecting to a SQL server database, and I'm doing this with django-pyodbc-azure. I noticed that my query performance was incredibly slow in a lot of places. </p>

<p>For a simple query where I was just selecting 50 rows, it would be taking like 11</p>]]></description><link>http://104.236.78.148/2019/12/06/fixing-my-dead-slow-sql-server-pyodbc-query-performance-2/</link><guid isPermaLink="false">c04dd8c9-020f-47e2-8012-80ff8e1884ef</guid><dc:creator><![CDATA[Kyle Valade]]></dc:creator><pubDate>Fri, 06 Dec 2019 19:14:37 GMT</pubDate><content:encoded><![CDATA[<p>In <a href="http://104.236.78.148/2019/10/16/working-with-unmanaged-sql-server-in-django-pt-ii/">one of my Django projects</a>, I'm connecting to a SQL server database, and I'm doing this with django-pyodbc-azure. I noticed that my query performance was incredibly slow in a lot of places. </p>

<p>For a simple query where I was just selecting 50 rows, it would be taking like 11 seconds to complete. That's after making sure all of the relevant columns were indexed. At first I thought that maybe the Django Rest Framework performance was a <em>lot</em> worse than I remembered. Pagination, maybe? But digging in, it became clear that it was just the query execution.</p>

<p>Alright, so here's an example query:</p>

<pre><code>SELECT TOP 50 * FROM Customer WHERE email LIKE '%you@gmail.com%'  
</code></pre>

<p>The email itself was actually parameterized in Django/pyodbc, so it would be sent more like the following pseudocode:</p>

<pre><code>params = ('you@gmail.com',)  
query = "SELECT TOP 50 * FROM Customer WHERE email LIKE '%' + %s + '%'"  
rows = cursor.execute(query, params)  
</code></pre>

<p>It was strange because I'd try hardcoding the parameters in the query so it would be more like the top one and the performance was fine. Thus it had to be something with the parameters.</p>

<p>And it was! A Google query led me to some suspicious looking unicode talk in the pyodbc wiki. My database is encoded as something like latin1, while pyodbc converts everything to unicode by default. Meaning that, db-side, parameters were being cast from utf8 strings to latin1, which kills the performance of the indexes. <a href="https://github.com/mkleehammer/pyodbc/wiki/Unicode">This article</a> pretty much gives the fix. Essentially:</p>

<blockquote>
  <p>Check your SQL Server collation using:</p>
  
  <p><code>select serverproperty('collation')</code></p>
  
  <p>If it is something like "SQL<em>Latin1</em>General<em>CP1</em>CI_AS" and you want str results, you may try:</p>
</blockquote>

<pre><code>cnxn.setdecoding(pyodbc.SQL_CHAR, encoding='latin1', to=str)  
cnxn.setencoding(str, encoding='latin1')  
</code></pre>

<p>And that was it, really. But how to apply that to django-pyodbc-azure? I ended up overriding the db engine. Django expects you to put this as <code>base.py</code> in its own package.</p>

<pre><code># utils.db_engine.base

from datetime import datetime

import pyodbc  
from sql_server.pyodbc.base import DatabaseWrapper as PyodbcDatabaseWrapper


# Inspired by https://github.com/michiya/django-pyodbc-azure/issues/160

class DatabaseWrapper(PyodbcDatabaseWrapper):

    def get_new_connection(self, conn_params):
        """
        Need to do this because pyodbc sends query parameters as unicode by default,
        whereas our server is latin1.

        Refs:
        [1]: https://github.com/mkleehammer/pyodbc/wiki/Unicode
        [2]: https://github.com/mkleehammer/pyodbc/issues/376
        [3]: https://github.com/mkleehammer/pyodbc/issues/112
        """
        connection = super().get_new_connection(conn_params)
        connection.setdecoding(pyodbc.SQL_CHAR, encoding='latin1')
        connection.setencoding(encoding='latin1')

        return connection
</code></pre>

<p>And then change your settings</p>

<pre><code># settings.py

DATABASES = {  
    'default': {
        'ENGINE': 'utils.db_engine',
        ...
    }
}
</code></pre>

<p>And that was pretty good. That brought lots of queries down to milliseconds. But a few of them were still disturbingly high - particularly the ones without many results. Some of those still took about 3 seconds.</p>

<p>Can you guess what the issue is?</p>

<p>Let's look at the query again: </p>

<pre><code>SELECT TOP 50 * FROM Customer WHERE email LIKE '%you@gmail.com%'  
</code></pre>

<p>The leading wildcard essentially turns the query into a full-text search, which also kills the index. Turns it into a scan instead of a seek, which isn't nearly as good. There aren't a lot of great options for this in SQL Server. You can create a full-text index on all of the fields where you'll be doing these types of searches. In my case, I decided that it's probably fine to just do a startswith query and drop the leading wildcard. In Django Rest Framework, this was pretty easy. In my viewset, I just added a carrot to the beginning of my <code>search_fields</code>.</p>

<pre><code>class CustomerViewSet(viewsets.ReadOnlyModelViewSet):

    search_fields = ['^email', '^first_name', '^last_name']
</code></pre>

<p>And then I was happy.</p>]]></content:encoded></item><item><title><![CDATA[Django in Azure Web Apps - too many redirects]]></title><description><![CDATA[<p>I've been testing out Azure Web App services lately so I can avoid all of the reverse proxy/server management mumbo jumbo. </p>

<p>When I was deploying my Django app, I hit this issue. I'd try to go to the site in Chrome, but got a "Too many redirects" error. I</p>]]></description><link>http://104.236.78.148/2019/10/25/django-in-azure-web-apps-too-many-redirects/</link><guid isPermaLink="false">71637d68-bba5-405f-a911-781fe12653a8</guid><dc:creator><![CDATA[Kyle Valade]]></dc:creator><pubDate>Fri, 25 Oct 2019 18:10:35 GMT</pubDate><content:encoded><![CDATA[<p>I've been testing out Azure Web App services lately so I can avoid all of the reverse proxy/server management mumbo jumbo. </p>

<p>When I was deploying my Django app, I hit this issue. I'd try to go to the site in Chrome, but got a "Too many redirects" error. I tried turning off HTTPS redirection in the Azure portal, but to no avail. Nothing in my Sentry logs. The other logs all looked fine, too.</p>

<p>Finally, after like 2 hours of re-looking at my logs, googling, and blaming gunicorn, I looked back at my django settings. Turns out I had done some premature securing of the website and had added <code>SECURE_SSL_REDIRECT = True</code>. So, of course, that was the issue. As soon as I switched that to <code>False</code> and redeployed, the issue disappeared.</p>

<p>There you have it. Make sure that <code>SECURE_SSL_REDIRECT = False</code>. Azure can do all that for you, anyways.</p>]]></content:encoded></item><item><title><![CDATA[Working with SQL Server in Django pt. II]]></title><description><![CDATA[<p>This is a follow-up and brutal takedown of a post I wrote about two years ago. <a href="http://nightlyclosures.com/2018/01/08/working-with-mssql-in-django/">Go here for part I</a>.</p>

<p>I'm starting to use Django again lately and am integrating with our data warehouse, contrary to my own apparent recommendation against that from a couple of years ago. None</p>]]></description><link>http://104.236.78.148/2019/10/16/working-with-unmanaged-sql-server-in-django-pt-ii/</link><guid isPermaLink="false">1c463a02-3b2f-4557-ab7a-95b7190dd656</guid><dc:creator><![CDATA[Kyle Valade]]></dc:creator><pubDate>Wed, 16 Oct 2019 14:39:24 GMT</pubDate><content:encoded><![CDATA[<p>This is a follow-up and brutal takedown of a post I wrote about two years ago. <a href="http://nightlyclosures.com/2018/01/08/working-with-mssql-in-django/">Go here for part I</a>.</p>

<p>I'm starting to use Django again lately and am integrating with our data warehouse, contrary to my own apparent recommendation against that from a couple of years ago. None of this is in production yet, mind you. Just like before. But this time, development is going a lot smoother.</p>

<h2 id="lastepisode">Last episode...</h2>

<p>What were the issues we were facing before?</p>

<ul>
<li>We have two databases - the app database and an external one, which is our data warehouse. The data warehouse is what we'll be focusing on here.</li>
<li>our data warehouse is SQL server, which isn't supported by Django out of the box
<ul><li>Using SQL server itself isn't too bad because there's a decent package to help with that - <a href="https://github.com/michiya/django-pyodbc-azure">django-pyodbc-azure</a> - though it's a little behind. </li></ul></li>
<li>and the database uses schemas, which Django abhors.
<ul><li>This takes some code</li></ul></li>
<li>and the database is unmanaged, which isn't fun for testing and local development
<ul><li>More code to get around this</li></ul></li>
</ul>

<h2 id="thejourney">The journey</h2>

<h3 id="twodatabases">Two databases</h3>

<p>That one's pretty easy. See <a href="https://docs.djangoproject.com/en/2.2/topics/db/multi-db/">Django's docs</a>. The main thing here is that you have to create a Database Routing class, which is just a class with a few methods. It would extend an interface if this were any other language, but in this case it doesn't even have to do that.</p>

<pre><code># database_router.py

from django.conf import settings


class DatabaseRouter:

    def db_for_read(self, model, **hints):
        return getattr(model, 'database', 'default')

    def db_for_write(self, model, **hints):
        return getattr(model, 'database', 'default')

    def allow_relation(self, obj1, obj2, **hints):
        return True

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if db == 'default':
            return True

        return settings.ALLOW_WAREHOUSE_DB_MIGRATION
</code></pre>

<p>And add your router to your settings. We'll look at your db settings next.</p>

<pre><code># settings.py

DATABASE_ROUTERS = ['utils.database_routers.DatabaseRouter']  
</code></pre>

<p>The only trick here was to determine which database each model maps to, as I couldn't find an attribute to use by default. So I created one. Initially, I just used a static attribute in each model, like so:</p>

<pre><code># models.py

class MyModel(models.Model):  
    database = 'data_warehouse'

    ...
</code></pre>

<p>That's a bit of a pain to remember to do and gets fixed up later on, but the router still needs that attribute.</p>

<h3 id="djangosqlserver">Django + SQL server</h3>

<p>Again, <a href="https://github.com/michiya/django-pyodbc-azure">django-pyodbc-azure</a>.</p>

<p><code>pip install django-pyodbc-azure</code></p>

<pre><code># settings.py

DATABASES = {  
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    },
    'data_warehouse': {
        'ENGINE': 'sql_server.pyodbc',
        'NAME': 'DataWarehouse',
        'HOST': '192.168.1.1',
        'PORT': '1433',
        'USER': config.get('database', 'username'),
        'PASSWORD': config.get('database', 'password'),
        'AUTOCOMMIT': False,  # Just cuz I treat this as read-only
        'OPTIONS': {
            'driver': 'ODBC Driver 13 for SQL Server',
            'host_is_server': True,
        }
    }
}
</code></pre>

<h3 id="creatingamodel">Creating a model</h3>

<p>So now you're about ready to map out some models to your data warehouse, right?</p>

<pre><code># models.py

class Customer(models.Model):

    database = 'data_warehouse'

    id = models.AutoField(primary_key=True, db_column='Id')
    name = models.CharField(max_length=128, db_column='FullName')

    class Meta:
        managed = False
        db_table = ...?
</code></pre>

<p>Hint: remember to use <code>AutoField</code> for the primary key (if it's your standard auto-incrementing pk) - otherwise when you do <code>Consumer.objects.create()</code>, the id won't auto-populate and you'll waste two hours trying to get your tests to work.</p>

<p>Anyways...by golly, that table is in a schema, how do we specify the schema we want?</p>

<p>That's an ugly bit, but manageable. It involves a bit of SQL injection. But...the good kind...?</p>

<p><code>db_table = '&lt;schema name&gt;].[&lt;table name&gt;</code></p>

<p>so our metaclass would be...</p>

<pre><code>class Customer(models.Model):

    class Meta:
        managed = False
        db_table = 'B2C].[Customer'
</code></pre>

<p>See? Not too bad. Not <em>fun</em>. But not too bad.</p>

<h3 id="regroup">Regroup!</h3>

<p>Cool, so now we can read from our database. And that's fine, if we have a pre-populated database to read from. But what if we want to run some tests? </p>

<p>Well...the database is unmanaged, so migrations won't be applied to our test db. Fine, so turn off migrations and have django do the equivalent of a sync db. Cool, but our models are still unmanaged so we'll run into issues.</p>

<p>What else? Oh yeah, there's still that ugly stuff we have to do for each data warehouse model. And there are a lot of such models.</p>

<h3 id="testing">Testing</h3>

<p>This ended up being fine. Just some code.</p>

<p>First, we'll disable migrations. Turns out it's not as easy as <code>SOUTH_DISABLE_MIGRATIONS = True</code> anymore, but more dynamic, I guess. So, in your settings or wherever:</p>

<pre><code># settings.py

class DisableMigrations(object):

    def __contains__(self, item):
        return True

    def __getitem__(self, item):
        return None


MIGRATION_MODULES = DisableMigrations()  
</code></pre>

<p>Easy? Easy.</p>

<p>To tell Django that you want your model to be managed during tests, you just need to write a custom TestRunner that goes through all of your models, checks their meta, and changes the <code>managed</code> attribute if necessary. I really just copied/pasted this code from <a href="https://gist.github.com/NotSqrt/5f3c76cd15e40ef62d09">here</a> or <a href="https://gist.github.com/raprasad/f292f94657728de45d1614a741928308">here</a></p>

<pre><code># settings.py

class UnManagedModelTestRunner(DiscoverRunner):  
    '''
    Test runner that automatically makes all unmanaged models in your Django
    project managed for the duration of the test run.
    Many thanks to the Caktus Group: http://bit.ly/1N8TcHW
    '''

    def setup_test_environment(self, *args, **kwargs):
        from django.apps import apps
        self.unmanaged_models = [m for m in apps.get_models() if not m._meta.managed]

        for m in self.unmanaged_models:
            m._meta.managed = True

        super(UnManagedModelTestRunner, self).setup_test_environment(*args, **kwargs)

    def teardown_test_environment(self, *args, **kwargs):
        super(UnManagedModelTestRunner, self).teardown_test_environment(*args, **kwargs)
        for m in self.unmanaged_models:
            m._meta.managed = False


TEST_RUNNER = 'my_project.settings.UnManagedModelTestRunner'  
</code></pre>

<p>Cool, so now you can run your tests and save your test objects to the database. The rest is really just icing.</p>

<h3 id="abstracttheboilerplate">Abstract the boilerplate</h3>

<p>This part was kind of fun because I had never gotten to mess around with meta classes before. Essentially, I wanted to add a <code>schema</code> attribute to Meta, and then have the base class do the sql injection for me. Then that garbage isn't littered all around the code. Also not having to specify the database would be nice.</p>

<p>I ended up creating a <code>DataWarehouseModel</code> that extends <code>model.Model</code> and takes care of all that for me.</p>

<pre><code>from django.db.models.base import ModelBase  
from django.db import models  
from django.conf import settings


DEFAULT_DB_FORMAT = '{schema}__{table}'

# Ugly string injection hack so that we can access the table under the schema
# See: http://kozelj.org/django-1-6-mssql-and-schemas/
SQL_DB_FORMAT = '{schema}].[{table}'


class DataWarehouseMeta(ModelBase):

    def __new__(typ, name, bases, attrs, **kwargs):
        super_new = super().__new__

        # Also ensure initialization is only performed for subclasses of Model
        # (excluding Model class itself).
        parents = [b for b in bases if isinstance(b, DataWarehouseMeta)]
        if not parents:
            return super_new(typ, name, bases, attrs)

        meta = attrs.get('Meta', None)
        if not meta:
            meta = super_new(typ, name, bases, attrs, **kwargs).Meta

        # ignore abstract models
        is_abstract = getattr(meta, 'abstract', False)
        if is_abstract:
            return super_new(typ, name, bases, attrs, **kwargs)

        # Ensure table is unmanaged unless explicitly set
        is_managed = getattr(meta, 'managed', False)
        meta.managed = is_managed

        # SQL injection garbage
        meta.db_table = typ.format_db_table(bases, meta)

        # Delete my custom attributes so the Meta validation will let the server run
        del meta.warehouse_schema
        del meta.warehouse_table

        attrs['Meta'] = meta
        return super().__new__(typ, name, bases, attrs, **kwargs)

    @classmethod
    def format_db_table(cls, bases, meta):
        table_format = DEFAULT_DB_FORMAT

        model_database = bases[0].database
        db_settings = settings.DATABASES.get(model_database)
        engine = db_settings['ENGINE']

        if engine == 'sql_server.pyodbc':
            table_format = SQL_DB_FORMAT

        return table_format.format(
            schema=meta.warehouse_schema,
            table=meta.warehouse_table
        )


class DataWarehouseModel(models.Model, metaclass=DataWarehouseMeta):

    database = 'data_warehouse'

    class Meta:
        abstract = True
</code></pre>

<p>And that class is used like so:</p>

<pre><code># models.py

class Customer(DataWarehouseModel):

    id = models.IntegerField(...)

    class Meta:
        warehouse_schema = 'B2C'
        warehouse_table = 'Customer'
</code></pre>

<p>And that's it.</p>

<p>What <code>DataWarehouseMeta</code> is doing is:</p>

<ul>
<li>setting <code>managed = False</code> by default</li>
<li>generating <code>db_table</code> from our custom attributes (<code>warehouse_schema</code> + <code>warehouse_table</code>)</li>
<li>setting <code>db_table</code> to what will be passed to the super class - Django's Meta implementation</li>
<li>deleting the custom attributes, since Django makes sure there aren't any extra attributes lying around its Meta class</li>
</ul>

<p>This <a href="https://stackoverflow.com/questions/725913/dynamic-meta-attributes-for-django-models/727956#727956">StackOverflow answer</a> was a lot of help.</p>

<h2 id="summary">Summary</h2>

<p>So that's it! Nothing a little elbow grease can't fix. I now officially endorse using Django with SQL Server - even complex ones with schemas. The caveat is that we are relying on django-pyodbc-azure, which is a couple of versions behind.</p>

<p>If I get a basic example repo going on GitHub, I'll link it here later.</p>]]></content:encoded></item><item><title><![CDATA[Calculate grouped YTD totals for previous years in Pandas]]></title><description><![CDATA[<p>I want to calculate the YTD total for the last couple years for every customer + product combination that has been sold. I'm new to Pandas and actually spent kind of a long time on this problem, but the solution turned out to be pretty simple. Assume today is August 1,</p>]]></description><link>http://104.236.78.148/2019/08/01/calculate-grouped-ytd-totals-for-previous-years-in-pandas/</link><guid isPermaLink="false">29d2fc1d-3569-4826-a7ca-d426a90fa1d7</guid><dc:creator><![CDATA[Kyle Valade]]></dc:creator><pubDate>Thu, 01 Aug 2019 13:28:08 GMT</pubDate><content:encoded><![CDATA[<p>I want to calculate the YTD total for the last couple years for every customer + product combination that has been sold. I'm new to Pandas and actually spent kind of a long time on this problem, but the solution turned out to be pretty simple. Assume today is August 1, 2019.</p>

<p>I have data that looks like...</p>

<pre><code>|   | OrderDate  | Customer | Product | OrderAmount |
|---|------------|----------|---------|-------------|
| 1 | 2018-02-10 | 1        | 10      | 10.00       |
| 2 | 2018-05-11 | 2        | 11      | 5.00        |
| 3 | 2018-09-10 | 1        | 10      | 10.00       |  # Don't include in YTD!
</code></pre>

<p>At the end, I want a dataframe that looks something like this:</p>

<pre><code>|   | Customer | Product | 2018_total | 2017_total |
|---|----------|---------|------------|------------|
| 1 | 1        | 10      | 10.00      | 0          |
| 2 | 2        | 11      | 5.00       | 0          |
</code></pre>

<p>And it has to be performant because there's a lot of data. So <code>iterrows</code> is out, as is <code>groupby().apply()</code>, because that thing is ungodly slow (it was taking real <em>seconds</em> per group).</p>

<p>What I ended up doing was creating a year column (I cheated and got it from the DB), copying the columns that I wanted to index into new columns (probably cuz I'm a noob), and then just doing a <code>df.query().groupby().sum()</code> into a new column.</p>

<p>Now obviously you don't <em>need</em> a year - you could just do a <code>x &lt; y &lt; z</code>, but the year helped for other things, so it's staying, dammit.</p>

<p>So now our dataset looks like...</p>

<pre><code>| Index(Customer/Product) | OrderDate  | Customer | Product | OrderAmount | Year |
|-------------------------|------------|----------|---------|-------------|------|
| 1/10                    | 2018-02-10 | 1        | 10      | 10.00       | 2018 |
| 2/11                    | 2018-05-11 | 2        | 11      | 5.00        | 2018 |
| 1/10                    | 2018-09-10 | 1        | 10      | 10.00       | 2018 |
</code></pre>

<p>The below code shows how to do it all...</p>

<pre><code>df['CustomerKeyIndex'] = df['CustomerKey']  
df['ProductKeyIndex'] = df['ProductKey']  
df = df.set_index(['CustomerKeyIndex', 'ProductKeyIndex'])

query = 'Year == 2018 and OrderDate &lt;= "2018-08-01"'  
df['2018_YTD'] = df.query(query) \  
    .groupby(['CustomerKey', 'ProductKey'])['OrderAmount'] \
    .sum()

df = df[~df.index.duplicated(keep='first')]  # To get only a single Customer/Product combo  
</code></pre>

<p>Repeat for any other years you're looking for.</p>

<p>And that actually takes just a few seconds across a few million rows. I'm sure there's other ways of doing it (ie. time series lags across one year), but they seemed a bit more complicated and this was quick enough and fairly straightforward.</p>]]></content:encoded></item><item><title><![CDATA[Import a directory into Databricks using the Workspace API in Python]]></title><description><![CDATA[<p>Another fairly easy thing that I couldn't find in <a href="https://docs.azuredatabricks.net/api/latest/examples.html#import-a-notebook-or-directory">the docs</a>. I wanted to be able to upload a directory into my Databricks Workspace from my CI server so I could test the current branch.</p>

<p>Luckily enough, the <a href="https://github.com/databricks/databricks-cli">databricks-cli</a> library was written in Python, so we can just use that.</p>]]></description><link>http://104.236.78.148/2019/06/07/import-a-directory-into-databricks-using-the-workspace-api/</link><guid isPermaLink="false">4c655811-03c8-4ea2-a212-1ec56d750c65</guid><dc:creator><![CDATA[Kyle Valade]]></dc:creator><pubDate>Fri, 07 Jun 2019 17:52:59 GMT</pubDate><content:encoded><![CDATA[<p>Another fairly easy thing that I couldn't find in <a href="https://docs.azuredatabricks.net/api/latest/examples.html#import-a-notebook-or-directory">the docs</a>. I wanted to be able to upload a directory into my Databricks Workspace from my CI server so I could test the current branch.</p>

<p>Luckily enough, the <a href="https://github.com/databricks/databricks-cli">databricks-cli</a> library was written in Python, so we can just use that. But first you'll need to generate a token for yourself to use in the API. Of course, you need to follow <a href="https://docs.databricks.com/api/latest/authentication.html">the instructions</a> to be able to use the API in the first place, but from there it's pretty straightforward.</p>

<pre><code class="language-python">from databricks_cli.workspace.api import WorkspaceApi  
from databricks_cli.sdk.api_client import ApiClient


client = ApiClient(  
    host='https://your.databricks-url.net',
    token=api_key
)
workspace_api = WorkspaceApi(client)  
workspace_api.import_workspace_dir(  
    source_path=base_path,
    target_path="/Users/user@example.com/MyFolder",
    overwrite=True,
    exclude_hidden_files=True
)
</code></pre>]]></content:encoded></item><item><title><![CDATA[Read from SQL Server with Python/Pyspark in Databricks]]></title><description><![CDATA[<p>This is actually really easy, but not something spelled out explicitly in the <a href="https://docs.databricks.com/spark/latest/data-sources/sql-databases-azure.html">Databricks docs</a>, though it is mentioned in the <a href="https://spark.apache.org/docs/latest/sql-data-sources-jdbc.html">Spark docs</a>. Alas, SQL server always seems like it's a special case, so I tend to discount things unless they mention SQL server explicitly. Not this time!</p>

<p>I'm guessing</p>]]></description><link>http://104.236.78.148/2019/06/05/read-from-sql-server-with-python-in-databricks-2/</link><guid isPermaLink="false">583d1e98-c116-418a-bfca-f901ba4af840</guid><dc:creator><![CDATA[Kyle Valade]]></dc:creator><pubDate>Wed, 05 Jun 2019 13:39:35 GMT</pubDate><content:encoded><![CDATA[<p>This is actually really easy, but not something spelled out explicitly in the <a href="https://docs.databricks.com/spark/latest/data-sources/sql-databases-azure.html">Databricks docs</a>, though it is mentioned in the <a href="https://spark.apache.org/docs/latest/sql-data-sources-jdbc.html">Spark docs</a>. Alas, SQL server always seems like it's a special case, so I tend to discount things unless they mention SQL server explicitly. Not this time!</p>

<p>I'm guessing it's about as easy outside of Databricks if you're just running Pyspark. You'll just need to follow <a href="https://docs.azuredatabricks.net/spark/latest/data-sources/sql-databases-azure.html#azure-db">these docs</a> and install the proper library - <code>com.microsoft.azure:azure-sqldb-spark</code>.</p>

<p>But if you're using Databricks already, you don't even have to do that. Admittedly the performance isn't great, but that could be due to a thousand other factors that I have not yet looked into.</p>

<pre><code class="language-python">jdbc_url = "jdbc:sqlserver://{host}:{port};database={database};user={user};password={password};UseNTLMv2=true".format(  
  host=host, port=port, database=database, 
  user=dbutils.secrets.get('database', 'username'), 
  password=dbutils.secrets.get('database', 'password'))

df = (spark.read  
  .format("jdbc")
  .option("url", jdbc_url)
  .option("dbTable", "MyTable")
  .load()
)

display(df)  
</code></pre>]]></content:encoded></item></channel></rss>