<?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:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Ivan Trusov on Medium]]></title>
        <description><![CDATA[Stories by Ivan Trusov on Medium]]></description>
        <link>https://medium.com/@polarpersonal?source=rss-5308d3d5c68a------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*5JL-f1kF5jHFTM7Hr2qt9w.jpeg</url>
            <title>Stories by Ivan Trusov on Medium</title>
            <link>https://medium.com/@polarpersonal?source=rss-5308d3d5c68a------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 02:56:25 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@polarpersonal/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Implementing a todo web app with Python and Schorle framework]]></title>
            <link>https://polarpersonal.medium.com/implementing-a-todo-web-app-with-python-and-schorle-framework-62c1b16c39c0?source=rss-5308d3d5c68a------2</link>
            <guid isPermaLink="false">https://medium.com/p/62c1b16c39c0</guid>
            <category><![CDATA[server-driven-ui]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[htmx]]></category>
            <category><![CDATA[fastapi]]></category>
            <dc:creator><![CDATA[Ivan Trusov]]></dc:creator>
            <pubDate>Sun, 18 Feb 2024 21:30:29 GMT</pubDate>
            <atom:updated>2024-02-18T21:46:48.936Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/486/0*kd2rjXj4zxovMKfq.jpg" /><figcaption>Small Worlds by Wassily Kandinsky, 1922. Image source — <a href="https://www.wikiart.org/en/wassily-kandinsky/small-worlds-1922">Wikiart</a></figcaption></figure><p>In my last <a href="https://polarpersonal.medium.com/schorle-testing-the-waters-with-a-python-server-driven-ui-kit-053f85ee6574">post</a>, I mentioned my concept of a Pure Python UI library called Schorle. I’ve put some more effort into it, making the API a bit more Pythonic and simplifying it for rendering purposes.</p><p>Let’s see how a to-do list web app can be implemented with almost Pure Python (we still use a bit of CSS for styling, but it’s pretty much inevitable).</p><p><strong>What’s new?</strong></p><p>Recent changes I’ve made provide capabilities for quite interesting functionality:</p><ol><li>Conditional rendering</li><li>Rendering of lists</li><li>Suspense and loading fallback</li></ol><p>All these features play quite nicely with the example I’ve implemented. You can take a look at the full source code <a href="https://github.com/renardeinside/schorle/blob/main/examples/todo_list.py">here</a>, and play with it a bit by installing the schorle <a href="https://schorle.tech/">package</a>.</p><p><strong>Components</strong></p><p>While cooking Schorle, I’ve taken some good practices from other UI libraries. One of the abstractions I like in particular is the abstraction of a Component from React. Practically, it allows to combine various HTML elements, as well as other components into logical blocks.</p><p>However, one thing that I don’t like about components in React is that you need to use JSX to add some logic (e.g. if-else or for each operation). Considering these ideas, I came up with the following API in Python:</p><pre>class TodoView(Component):<br>    state: State<br>    classes: Classes = Classes(&quot;max-w-96 w-2/3 space-y-2 flex flex-col items-center&quot;)<br><br>    def render(self):<br>        with p(classes=Classes(&quot;text-xl&quot;)):<br>            _text = &quot;No todos yet.&quot; if not self.state.todos else &quot;Your todos:&quot;<br>            text(_text)<br><br>        for todo in self.state.todos:<br>            with div(classes=Classes(&quot;flex flex-row items-center justify-between w-full&quot;)):<br>                with p():<br>                    text(todo)<br>                with Button(<br>                    on=On(&quot;click&quot;, partial(self.state.remove, todo)),<br>                    modifier=&quot;error&quot;,<br>                ):<br>                    text(&quot;Delete&quot;)<br><br>    def initialize(self):<br>        self.suspense = Suspense(on=self.state, fallback=Loading())<br>        self.bind(self.state)</pre><p>Let’s stop here for a second to consider what’s happening.</p><p>In the render method of a component, the layout is described. Since it’s a valid Python code, any standard Pythonic construction can be used, for example, the if-else conditioning on the text, or a for loop to display all of the todos.</p><p>The state class provides access for read and write operations on the page state. Using the On class, it’s quite easy to define a server-side callback to the click trigger.</p><p>Indeed, the component needs to be re-rendered on any change of the state. To do so, thebind method is used. Also, since the operations on the state might take time, suspense is added to the component.</p><p><strong>What’s happening in the background?</strong></p><p>From the client side, prepared HTML will look like this:</p><pre>&lt;div class=&quot;max-w-96 w-2/3 space-y-2 flex flex-col items-center&quot; id=&quot;sle-div-7be95dec&quot; hx-swap-oob=&quot;morph&quot;&gt;<br>    &lt;p class=&quot;text-xl&quot; hx-swap-oob=&quot;morph&quot;&gt;Your todos:&lt;/p&gt;<br>    &lt;div class=&quot;flex flex-row items-center justify-between w-full&quot; hx-swap-oob=&quot;morph&quot;&gt;<br>        &lt;p hx-swap-oob=&quot;morph&quot;&gt;Buy milk&lt;/p&gt;<br>        &lt;button class=&quot;btn btn-error&quot; hx-swap-oob=&quot;morph&quot; ws-send=&quot;&quot; hx-trigger=&quot;click&quot; id=&quot;sle-059f8171&quot;&gt;Delete&lt;/button&gt;<br>    &lt;/div&gt;<br>    &lt;div class=&quot;flex flex-row items-center justify-between w-full&quot; hx-swap-oob=&quot;morph&quot;&gt;<br>        &lt;p hx-swap-oob=&quot;morph&quot;&gt;Do laundry&lt;/p&gt;<br>        &lt;button class=&quot;btn btn-error&quot; hx-swap-oob=&quot;morph&quot; ws-send=&quot;&quot; hx-trigger=&quot;click&quot; id=&quot;sle-a561de43&quot;&gt;Delete&lt;/button&gt;<br>    &lt;/div&gt;<br>&lt;/div&gt;</pre><p>The generated IDs are used for client-server communication. For instance, deleting an item will cause the following message:</p><pre>{<br>    &quot;HEADERS&quot;: {<br>        &quot;HX-Request&quot;: &quot;true&quot;,<br>        &quot;HX-Trigger&quot;: &quot;sle-e164c325&quot;,<br>        &quot;HX-Trigger-Name&quot;: null,<br>        &quot;HX-Target&quot;: &quot;sle-e164c325&quot;,<br>        &quot;HX-Current-URL&quot;: &quot;http://127.0.0.1:8000/&quot;,<br>        &quot;HX-Trigger-Type&quot;: &quot;click&quot;<br>    }<br>}</pre><p>At first, the server will respond with a loading view:</p><pre>&lt;div id=&quot;sle-div-ed017a60&quot; hx-swap-oob=&quot;morph&quot;&gt;<br>  &lt;div id=&quot;sle-div-7f1e0849&quot;&gt;<br>    &lt;span class=&quot;loading loading-lg loading-infinity&quot;&gt;&lt;/span&gt;<br>  &lt;/div&gt;<br>&lt;/div&gt;</pre><p>And then, as soon as the operation is finished, a proper response will come:</p><pre>&lt;div class=&quot;max-w-96 w-2/3 space-y-2 flex flex-col items-center&quot; id=&quot;sle-div-ed017a60&quot; hx-swap-oob=&quot;morph&quot;&gt;<br>  &lt;p class=&quot;text-xl&quot; hx-swap-oob=&quot;morph&quot;&gt;Your todos:&lt;/p&gt;<br>  &lt;div class=&quot;flex flex-row items-center justify-between w-full&quot; hx-swap-oob=&quot;morph&quot;&gt;<br>    &lt;p hx-swap-oob=&quot;morph&quot;&gt;Buy milk&lt;/p&gt;<br>    &lt;button class=&quot;btn btn-error&quot; hx-swap-oob=&quot;morph&quot; ws-send=&quot;&quot; hx-trigger=&quot;click&quot; id=&quot;sle-ff9a97ae&quot;&gt;Delete&lt;/button&gt;<br>  &lt;/div&gt;<br>&lt;/div&gt;</pre><p>On the UI, all of this will look as follows:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/772/1*DtKGRnigZaq7UjrZPMY5xg.gif" /><figcaption>Example of a todo list</figcaption></figure><p><strong>State operations</strong></p><p>Since I’ve mentioned state, let’s take a look at the implementation of it:</p><pre>class State(ReactiveModel):<br>    current: str = &quot;&quot;<br>    todos: list[str] = Field(default_factory=lambda: [&quot;Buy milk&quot;, &quot;Do laundry&quot;])<br><br>    @effector<br>    async def set_current(self, value):<br>        self.current = value<br><br>    @effector<br>    async def add_todo(self):<br>        await asyncio.sleep(random() * 2)  # simulate a slow backend<br>        self.todos.append(self.current)<br>        await self.set_current(&quot;&quot;)<br><br>    @effector<br>    async def remove(self, todo):<br>        await asyncio.sleep(random() * 2)  # simulate a slow backend<br>        self.todos.remove(todo)</pre><p>ReactiveModel is a class provided by the schorle framework. To simplify the explanation, consider it as a standard Pydantic BaseModel, with some necessary additions to make @effector work.</p><p>The @effector decorator highlights that this method has effects on the UI. Calling it outside of the schorle context, without any binds won’t affect the logic of the methods inside.</p><p><strong>Text input</strong></p><p>Since we also need to match the input together with the “<em>Add</em>” button, we need some way to exchange the input information with the server. Fortunately, we can use the same state class to do so, and add another UI component to combine this all together:</p><pre>class InputSection(Component):<br>    state: State<br><br>    def render(self):<br>        with div(classes=Classes(&quot;flex flex-row w-3/4 justify-center items-center space-x-4&quot;)):<br>            TextInput(<br>                value=self.state.current,<br>                placeholder=&quot;Add a todo&quot;,<br>                classes=Classes(&quot;input-bordered&quot;),<br>                on=On(&quot;change&quot;, self.state.set_current),<br>            )<br>            with Button(on=On(&quot;click&quot;, self.state.add_todo), modifier=&quot;primary&quot;):<br>                text(&quot;Add&quot;)<br><br>    def initialize(self):<br>        self.bind(self.state)</pre><p>Since the render is triggered only when any effectors on the state are fired, it will always correctly display the value of the input. When the user clicks Add, the value saved into current is used to add the todo.</p><p><strong>Summary</strong></p><p>Altogether, it took <a href="https://github.com/renardeinside/schorle/blob/main/examples/todo_list.py">only 98 lines of code</a> to prepare quite a functional to-do list using the framework, which I find particularly good.</p><p>Take note that we don’t use any frontend code, there is no need to JSONify the inputs back and forth between the server and the client. In this sense, the Schorle framework closely follows the <a href="https://en.wikipedia.org/wiki/HATEOAS#:~:text=Hypermedia%20as%20the%20engine%20of,provide%20information%20dynamically%20through%20hypermedia.">Hypermedia As The Engine Of Application State (HATEAOS)</a> principles.</p><p>What I find the most interesting is that with such an architecture developers can immediately interop with the inputs on the server side. For instance, to add the todo information into a database, the only required thing is to add saving methods to the State class defined above.</p><p>Under the hood, schorle indeed stands on the shoulders of giants:</p><ul><li><a href="https://fastapi.tiangolo.com/">FastAPI</a> — web framework</li><li><a href="https://htmx.org/">HTMX</a> — client-side library for dynamic HTML</li><li><a href="https://tailwindcss.com/">Tailwind CSS</a> — CSS framework</li><li><a href="https://daisyui.com/">DaisyUI</a> — Component library for Tailwind CSS</li><li><a href="https://docs.pydantic.dev/latest/">Pydantic</a> — classes, and utilities for elements</li></ul><p>I find a combination of FastAPI and HTMX very powerful, and the idea of the framework is to expose this power to the developers without having too much hassle with various low-level details.</p><p>CSS styling is almost inevitable for a properly extensible framework, and here Tailwind + DaisyUI seems like the best fit from the existing toolset.</p><p><em>If you’re interested in the concepts or ideas implemented in Schorle, I would be happy to hear your feedback. Try it out yourselves, and let me know if there are any issues or ideas on what could make it a better framework ☺️</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=62c1b16c39c0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Schorle: Testing the Waters with a Python Server-Driven UI Kit]]></title>
            <link>https://polarpersonal.medium.com/schorle-testing-the-waters-with-a-python-server-driven-ui-kit-053f85ee6574?source=rss-5308d3d5c68a------2</link>
            <guid isPermaLink="false">https://medium.com/p/053f85ee6574</guid>
            <category><![CDATA[server-driven-ui]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[async]]></category>
            <dc:creator><![CDATA[Ivan Trusov]]></dc:creator>
            <pubDate>Sun, 28 Jan 2024 19:23:56 GMT</pubDate>
            <atom:updated>2024-01-29T13:05:52.129Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kf6D0WkjZLscNL717kkcaQ.png" /></figure><p><strong>Preface</strong></p><p>I’ve built a small, and not-yet-polished asynchronous framework in Python for server-side UI development, called schorle . It lacks a lot of features of mature frameworks, but it has several things that are conceptually different from the existing solutions.</p><p>In this blog post, I would like to consider these differences, describe the concepts, and maybe spark some community interest. If after reading you’ll feel the urge to try out (or maybe even contribute), take a look at the <a href="https://github.com/renardeinside/schorle/tree/main">repo with the source code</a>.</p><p><strong>Existing approaches in JS and Python</strong></p><p>Modern web UIs inevitably use one of the following approaches at their core:</p><ul><li>CSR (Client-Side Rendering)</li><li>SSR (Server-Side Rendering)</li></ul><p>Examples of CSR frameworks are React, Vue, and Svelte. These are great technologies that deliver an uncompromised level of UI controls and convenience for the developers. However, from a backend developer point of view, they require not only knowing the basics of JS (or TS) but also quite a good understanding of how the browser works, loads the pages, etc.</p><p>These frameworks are reaching out to the server side, e.g. to request data or interact with the server. To perform such interactions, developers on the front-end side use browser APIs like fetch to retrieve and push data back to the server.</p><p>On the other hand, there is the SSR approach, when the server renders the whole page into an HTML payload and returns it as a response to the request from the browser.</p><p>Indeed all of these frameworks support SSR in some or another way, and there is a particularly interesting feature in the Next.JS framework called <a href="https://nextjs.org/docs/pages/building-your-application/data-fetching/incremental-static-regeneration">ISR</a>.</p><p>Such cool features of the JS ecosystem show a valid path for Server-Side applications in other languages, and there are many ways to do this in Python (the language I’m particularly used to).</p><p>Here are some examples of how this could be done:</p><ul><li>Rendering HTML pages from Jinja templates (e.g. in Flask, Django, FastAPI)</li><li>Using powerful UI frameworks such as <a href="https://github.com/plotly/dash">Dash</a> and <a href="https://github.com/streamlit">Streamlit</a></li></ul><p>However, I see three missing things in these approaches in Python:</p><ul><li><strong>Async</strong> and incremental page updates (with a strong accent on async)</li><li>Strong typing for the UI elements</li><li>Simple multi-page routing, auth, and other server-side features</li><li>Well-typed state management</li></ul><p>With these ideas in mind, I made a small PoC project schorle — and here is my conceptual vision of what a good Server-Driven UI kit in Python might look like.</p><p><strong>Concepts</strong></p><p>When it comes to writing a UI application, I’ve considered 3 main things that need to be covered:</p><ul><li>Elements and layout of the pages</li><li>Reacting to client-side events</li><li>Reacting to server-side state changes</li></ul><p>As per the list of “potential features” described above, I also would like to extend these definitions to:</p><ul><li>Well-typed elements and layouts, with the possibility of re-using them</li><li>Asynchronous reactivity for client-side events</li><li>Asynchronous reactivity for server-side state changes</li></ul><p>Let’s consider these 3 features in a simple example — a UI with 2 buttons and a shared state between them:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/914/1*hbvW_C7568u9fOZOQgPlbQ.gif" /><figcaption>Sample UI app built with Schorle</figcaption></figure><p>As we can see, this app has all three concepts in place:</p><ul><li>We have a layout of elements</li><li>Two of these elements are reactive to the client-side actions</li><li>One element is a “receiver” that changes when the server-side state is changed.</li></ul><p>Starting from the layout, let’s see how it’s managed in Schorle:</p><pre>from __future__ import annotations<br><br>from schorle.app import Schorle<br>from schorle.effector import effector<br>from schorle.elements.button import Button<br>from schorle.elements.html import Div<br>from schorle.elements.page import Page, PageReference<br>from schorle.reactives.classes import Classes<br>from schorle.reactives.state import ReactiveModel<br>from schorle.reactives.text import Text<br><br># snip<br><br>class DecrementButton(Button):<br>    text: Text = Text(&quot;Decrement&quot;)<br>    page: PageWithButton = PageReference()<br>    classes: Classes = Classes(&quot;btn-error&quot;)<br>    # snip<br><br><br>class Buttons(Div):<br>    classes: Classes = Classes(&quot;space-x-4 flex flex-row justify-center items-center&quot;)<br>    increment: Button = Button.factory(text=Text(&quot;Increment&quot;), classes=Classes(&quot;btn-success&quot;))<br>    decrement: DecrementButton = DecrementButton.factory()<br>    page: PageWithButton = PageReference()<br>    # snip<br><br><br>class CounterView(Div):<br>    # snip<br><br>class PageWithButton(Page):<br>    counter: Counter = Counter.factory()<br>    classes: Classes = Classes(&quot;space-y-4 flex flex-col justify-center items-center h-screen w-screen&quot;)<br>    buttons: Buttons = Buttons.factory()<br>    counter_view: CounterView = CounterView.factory()</pre><p>In the framework, elements are represented as a small subclass around pydantic BaseModel class. This provides a convenient way to introduce new elements via subclassing them, as well as an interface to combine them into more complex structures (e.g. nested Div with two buttons as per the code above).</p><p>Elements have two special fields, namely text and classes . These fields provide a convenient way to change the text and classes on the element.</p><p>Now, let’s add some reactivity to these components:</p><pre>class DecrementButton(Button):<br>    text: Text = Text(&quot;Decrement&quot;)<br>    page: PageWithButton = PageReference()<br>    classes: Classes = Classes(&quot;btn-error&quot;)<br><br>    async def _switch_off(self, counter: Counter):<br>        if counter.value &lt;= 0:<br>            await self.classes.append(&quot;btn-disabled&quot;)<br>        else:<br>            await self.classes.remove(&quot;btn-disabled&quot;)<br><br>class Buttons(Div):<br>    classes: Classes = Classes(&quot;space-x-4 flex flex-row justify-center items-center&quot;)<br>    increment: Button = Button.factory(text=Text(&quot;Increment&quot;), classes=Classes(&quot;btn-success&quot;))<br>    decrement: DecrementButton = DecrementButton.factory()<br>    page: PageWithButton = PageReference()<br><br>    async def before_render(self):<br>        self.increment.add_callback(&quot;click&quot;, self.page.counter.increment)<br>        self.decrement.add_callback(&quot;click&quot;, self.page.counter.decrement)<br><br>class CounterView(Div):<br>    page: PageWithButton = PageReference()<br><br>    async def update(self, counter: Counter):<br>        await self.text.update(f&quot;Clicked {counter.value} times&quot;)</pre><p>With this piece of code, we add reactivity to the following UI elements:</p><ul><li>The DecrementButton will become disabled as soon as the counter is equal to or less than zero</li><li>The counter view needs to be updated with the new value of the counter</li><li>Both buttons shall be reactive and therefore we add callbacks to them.</li></ul><p>This code is indeed not runnable, because we’re missing a class that represents the Counter itself. Let’s introduce it as follows:</p><pre>class Counter(ReactiveModel):<br>    value: int = 0<br><br>    @effector<br>    async def increment(self):<br>        self.value += 1<br><br>    @effector<br>    async def decrement(self):<br>        self.value -= 1</pre><p>This class seems quite strange at first glance. Why do functions that operate on the value have this strange decorator, and more importantly — why do they need to be async?</p><p>Indeed this is not a coincidence — this is where we introduce the feedback loop — when something changes on the <strong>server</strong>, we need to send updates back to the <strong>client</strong> UI.</p><p>We can think of such a server-side change as an effector (function which has effects), and the UI update in this case is an effect.</p><p>With this logical concept, we can introduce a way to connect the server-side actions, e.g. functional calls to the UI updates.</p><p>In schorle, it’s possible to <strong>subscribe</strong> other methods to the <strong>effectors </strong>as follows:</p><pre>class DecrementButton(Button):<br>    text: Text = Text(&quot;Decrement&quot;)<br>    page: PageWithButton = PageReference()<br>    classes: Classes = Classes(&quot;btn-error&quot;)<br><br>    async def before_render(self):<br>        await self.page.counter.decrement.subscribe(self._switch_off, trigger=True)<br>        await self.page.counter.increment.subscribe(self._switch_off, trigger=True)<br><br>    async def _switch_off(self, counter: Counter):<br>        if counter.value &lt;= 0:<br>            await self.classes.append(&quot;btn-disabled&quot;)<br>        else:<br>            await self.classes.remove(&quot;btn-disabled&quot;)<br><br>class CounterView(Div):<br>    page: PageWithButton = PageReference()<br><br>    async def update(self, counter: Counter):<br>        await self.text.update(f&quot;Clicked {counter.value} times&quot;)<br><br>    async def before_render(self):<br>        await self.page.counter.increment.subscribe(self.update, trigger=True)<br>        await self.page.counter.decrement.subscribe(self.update, trigger=True)</pre><p>In this example, we say that:</p><ul><li>Whenever an increment or decrement happens, we need to change the classes of the DecrementButton</li><li>Whenever an increment or decrement happens, we need to update the text of the counter view.</li></ul><p>Now we can put this logic together and have the following code:</p><pre>from __future__ import annotations<br><br>from schorle.app import Schorle<br>from schorle.effector import effector<br>from schorle.elements.button import Button<br>from schorle.elements.html import Div<br>from schorle.elements.page import Page, PageReference<br>from schorle.reactives.classes import Classes<br>from schorle.reactives.state import ReactiveModel<br>from schorle.reactives.text import Text<br><br>app = Schorle()<br><br><br>class Counter(ReactiveModel):<br>    value: int = 0<br><br>    @effector<br>    async def increment(self):<br>        self.value += 1<br><br>    @effector<br>    async def decrement(self):<br>        self.value -= 1<br><br><br>class DecrementButton(Button):<br>    text: Text = Text(&quot;Decrement&quot;)<br>    page: PageWithButton = PageReference()<br>    classes: Classes = Classes(&quot;btn-error&quot;)<br><br>    async def before_render(self):<br>        await self.page.counter.decrement.subscribe(self._switch_off, trigger=True)<br>        await self.page.counter.increment.subscribe(self._switch_off, trigger=True)<br><br>    async def _switch_off(self, counter: Counter):<br>        if counter.value &lt;= 0:<br>            await self.classes.append(&quot;btn-disabled&quot;)<br>        else:<br>            await self.classes.remove(&quot;btn-disabled&quot;)<br><br><br>class Buttons(Div):<br>    classes: Classes = Classes(&quot;space-x-4 flex flex-row justify-center items-center&quot;)<br>    increment: Button = Button.factory(text=Text(&quot;Increment&quot;), classes=Classes(&quot;btn-success&quot;))<br>    decrement: DecrementButton = DecrementButton.factory()<br>    page: PageWithButton = PageReference()<br><br>    async def before_render(self):<br>        self.increment.add_callback(&quot;click&quot;, self.page.counter.increment)<br>        self.decrement.add_callback(&quot;click&quot;, self.page.counter.decrement)<br><br><br>class CounterView(Div):<br>    page: PageWithButton = PageReference()<br><br>    async def update(self, counter: Counter):<br>        await self.text.update(f&quot;Clicked {counter.value} times&quot;)<br><br>    async def before_render(self):<br>        await self.page.counter.increment.subscribe(self.update, trigger=True)<br>        await self.page.counter.decrement.subscribe(self.update, trigger=True)<br><br><br>class PageWithButton(Page):<br>    counter: Counter = Counter.factory()<br>    classes: Classes = Classes(&quot;space-y-4 flex flex-col justify-center items-center h-screen w-screen&quot;)<br>    buttons: Buttons = Buttons.factory()<br>    counter_view: CounterView = CounterView.factory()<br><br><br>@app.get(&quot;/&quot;)<br>def get_page():<br>    return PageWithButton()</pre><p>I’m not entirely convinced that this is good enough (I assume some simplifications might still be applied), but I see some good start here:</p><ul><li>It follows the imperative paradigm of Python programming, explicitly defining what should be changed and when.</li><li>Functions that have side effects are defined explicitly, and the side effects are managed explicitly, almost following the Zen of Python in the concepts of Explicit is better than implicit .</li><li>Elements, effects, and functions are well-typed and the behavior is predictable.</li></ul><p>Logically, the flow of events looks like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*D0Am9yH77FCaIgHKn4BWwQ.png" /></figure><p>Under the hood, the following technologies are used:</p><ol><li><a href="https://htmx.org/">HTMX</a> on the client side to define triggers and communicate messages via WebSockets</li><li><a href="https://docs.pydantic.dev/latest/">Pydantic</a>-based classes to represent elements and state</li><li><a href="https://fastapi.tiangolo.com/">FastAPI</a> as a server backend</li><li><a href="https://tailwindcss.com/">TailwindCSS</a> and <a href="https://daisyui.com/">DaisyUI</a> for effortless styling</li></ol><p><strong>Summary</strong></p><p>The project is currently in a PoC state, meaning that I’m looking for more feedback and ideas from the Python community on how such things could be done better. Share your thoughts in the comments, or maybe even try creating some apps with it, and let me know how that feels from a developer perspective.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=053f85ee6574" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building an image classification app with Databricks Connect “V2” and Dash]]></title>
            <link>https://polarpersonal.medium.com/building-an-image-classification-app-with-databricks-connect-v2-and-dash-3c7f855f6e6?source=rss-5308d3d5c68a------2</link>
            <guid isPermaLink="false">https://medium.com/p/3c7f855f6e6</guid>
            <category><![CDATA[image-classification]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[dash]]></category>
            <category><![CDATA[databricks-connect]]></category>
            <category><![CDATA[databricks]]></category>
            <dc:creator><![CDATA[Ivan Trusov]]></dc:creator>
            <pubDate>Thu, 18 May 2023 18:10:16 GMT</pubDate>
            <atom:updated>2023-05-18T18:10:16.228Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/481/0*jbbVzEgZ6mqyhPGu.jpg" /><figcaption>The Large Figure Paintings, nr 5 by Hilma af Klimt, 1907. Image source -<a href="https://www.wikiart.org/en/hilma-af-klint/the-large-figure-paintings-nr-5-1907"> wikiart.org</a></figcaption></figure><p>Intelligent data application adoption is rapidly growing amongst various data platforms. And with this growing adoption, engineers are looking for efficient and flexible APIs to work with their data in a way they’re used to operating pipelines, but inside such data applications.</p><p>In this blog post, I’ll demonstrate how the <a href="https://www.databricks.com/blog/2023/04/18/use-databricks-anywhere-databricks-connect-v2.html">upgraded Databricks Connect</a> protocol (based on <a href="https://www.databricks.com/blog/2023/04/18/spark-connect-available-apache-spark.html">Spark Connect</a>) enables developers to write concise and expressive data applications on top of the Databricks Lakehouse Platform with the Dash framework.</p><p>⛓️ <strong>The Tale of Protocols</strong></p><p>There are several ways to perform operations with the data stored in the Databricks Lakehouse from the data application.</p><p>One of them is the one I’ve described in one of my <a href="https://polarpersonal.medium.com/building-a-data-labeling-app-with-python-dash-and-databricks-lakehouse-48341f618b8f">previous</a> blog posts — it’s a straightforward and well-known approach that uses DBSQL and the SQLAlchemy ORM mechanism.</p><p>However, this time I would like to highlight the (relatively) new functionality of Databricks, called <a href="https://www.databricks.com/blog/2023/04/18/use-databricks-anywhere-databricks-connect-v2.html">Databricks Connect “<em>V2</em>”</a>. Although for many of the Databricks users, this component may sound familiar, its new version provides a more flexible and faster approach within a thinner version of a client that can be potentially used from applications in various programming languages.</p><p>Why would a Data Engineer prefer using Databricks Connect over a standard ORM-like solution?</p><p>There are several reasons for that, and one of the most important for me is that most of the Data Engineers have very good knowledge of Apache Spark and its APIs, whilst SQLAlchemy is usually closer to the experience of Software Engineers.</p><p>Being a Data Engineer myself in the past, between the two APIs below I clearly know which one I would like to choose most:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IApe3fIKJaF8XIbXGFeR2g.png" /><figcaption>self-made via excalidraw.com</figcaption></figure><p>👁️ <strong>Solution overview</strong></p><p>The architecture for such an app is relatively simple — there are two main components, namely:</p><ul><li>the <strong>frontend</strong> app (written in the robust Dash framework in almost pure Python)</li><li>the <strong>backend</strong> (which turns out to be a Databricks Cluster)</li></ul><p>Connectivity and data exchange between the backend and frontend is achieved via DB Connect “V2” protocol and the <a href="https://pypi.org/project/databricks-connect/">databricks-connect</a> Python package.</p><p>The image metadata (e.g. classes) is stored in a <a href="http://delta.io/">Delta Table</a>, while the images themselves are stored in a cloud storage as files.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7tQQUHSLJ-jx5YHLWZP-qw.png" /><figcaption>self-made in excalidraw.com</figcaption></figure><p>The main benefits of this architecture are the following:</p><ul><li>Images can be stored and retrieved natively, without a need to put them into tables as blobs or binaries</li><li>The metadata of the image can be easily changed and modified due to Delta Lake UPDATE <a href="https://docs.delta.io/latest/delta-update.html">functionality</a></li><li>We don’t need to use a specific ORM and can perform the backend operations by using Spark API methods itself</li><li>Our frontend app can be fully written in Python, without a need to switch language contexts</li></ul><p>💻 <strong>Setting up the development environment</strong></p><p>Prerequisites for this step:</p><ul><li>[Local machine] JVM (I’m using JVM 11 in this example)</li><li>[Local machine] Poetry</li><li>[Local machine] <a href="https://docs.databricks.com/dev-tools/cli/index.html#set-up-authentication-using-a-databricks-personal-access-token">Databricks profile configured via databricks-cli</a></li><li>[Databricks] Databricks All-purpose Cluster with DBR 13.X or higher and Unity Catalog support</li></ul><p>Let’s start by organizing the project and the dev environment. Our app is a poetry -based Python <a href="https://python-poetry.org/">package</a>, and we can easily kick off the project with all dependencies as follows:</p><pre># create project dir<br>mkdir db-connect-v2-image-classification<br><br># move into project dir<br>cd db-connect-v2-image-classification<br><br># initialize poetry<br>poetry init -n \<br>  --dependency=&quot;databricks-connect=13.*&quot; \ <br>  --dependency=&quot;hydra-core=1.3.*&quot; \ # for configurations<br>  --dependency=&quot;databricks-sdk&quot; \ # for cluster ops<br>  --dependency=&quot;dash&quot; # frontend framework<br><br># add dev dependencies<br>poetry add -G dev black ruff isort<br><br># poetry references this file, let&#39;s keep it empty for this moment<br>touch README.md<br><br># add package directory<br>mkdir db_connect_v2_image_classification<br>touch db_connect_v2_image_classification/__init__.py<br># install project and it&#39;s deps<br>poetry install</pre><p>With these steps, we have a fully prepared Python app skeleton. Let’s step into the project and add the first bit that is responsible for preparing the metadata table:</p><pre>code . # open VS Code in the project directory</pre><p>Add a .gitignore file with the following contents:</p><pre># Byte-compiled / optimized / DLL files<br>__pycache__/<br>*.py[cod]<br>*$py.class<br><br># Distribution / packaging<br>*.egg-info/<br>build<br>dist<br><br># Unit test / coverage reports<br>.coverage<br>coverage.xml<br>junit/*<br>htmlcov/*<br><br># Caches<br>.pytest_cache/<br><br># VSCode<br>.vscode/<br><br># Idea<br>.idea/<br>*.iml<br><br># MacOS<br>.DS_Store</pre><p>And then init the git project:</p><pre>git init .</pre><p><em>Hint — you can immediately create a repo in GitHub if you have GitHub CLI installed and if you’re using Bash-like solution:</em></p><pre>gh repo create &quot;${PWD##*/}&quot; --private --source=. --remote=origin</pre><p>Now we have a proper project skeleton, and it’s pretty convenient to work with it. However our app will manage several configurations — both the connection-related ones, as well as ETL and data related ones.</p><p>🪄 <strong>Adding configurations</strong></p><p>One of the most convenient ways to handle configurations is to use the hydra-core library.</p><p>Let’s add the config-related logic:</p><pre># db_connect_v2_image_classification/configs.py<br><br>from dataclasses import dataclass, field<br>from typing import Optional<br><br>from databricks.sdk import WorkspaceClient<br><br><br>@dataclass<br>class ImageTableConfig:<br>    catalog: str = &quot;main&quot;<br>    database: str = &quot;default&quot;<br>    table: str = &quot;db_connect_v2_example_images_metadata&quot;<br><br>    # note - repr is used both inside print statements and when table name is provided to Spark APIs<br>    def __repr__(self) -&gt; str:<br>        return f&quot;{self.catalog}.{self.database}.{self.table}&quot;<br><br><br>@dataclass<br>class AppConfig:<br>    cluster_name: str<br>    cluster_id: str = field(init=False)<br>    image_table: ImageTableConfig = field(default_factory=ImageTableConfig)<br>    profile: Optional[str] = &quot;DEFAULT&quot;<br>    debug: Optional[bool] = True<br><br>    # defines is we want to overwite the table, fail when it exists or append data into it<br>    table_saving_mode: Optional[str] = &quot;error&quot;<br><br>    def __post_init__(self):<br>        _w = WorkspaceClient(profile=self.profile)<br>        found = list(filter(lambda c: c.cluster_name == self.cluster_name, _w.clusters.list()))<br>        assert found is not None, f&quot;Not found any clusters with name {self.cluster_name}&quot;<br>        assert len(found) == 1, f&quot;There are more than one cluster with name {self.cluster_name}&quot;<br>        self.cluster_id = found[0].cluster_id</pre><p>The code above introduces data classes and a logical structure for the application config. Since dataclasses have a convenient __post_init__ method, we can automatically resolve the provided cluster_name argument into a cluster_id by using the latest Databricks SDK for Python.</p><p>🌯 <strong>Wrapping the entrypoints</strong></p><p>With the configuration structure defined, it’s also important to add a bit of a glue code so hydrawould work with the configurations in a way it would be convenient for our app:</p><pre># db_connect_v2_image_classification/main_wrapper.py<br><br>from typing import Callable<br><br>import hydra<br>from hydra.core.config_store import ConfigStore<br>from omegaconf import DictConfig, OmegaConf, SCMode<br><br>from db_connect_v2_image_classification.configs import AppConfig<br><br><br>def main_wrapper():<br>    # prepares the config store for any entrypoints that will use this decorator<br>    # since we&#39;re using the same AppConfig both for table creation.<br>    # for frontend app, we&#39;ll simply re-use this function.<br>    cs = ConfigStore.instance()<br>    cs.store(name=&quot;base_config&quot;, node=AppConfig)<br><br>    # a decorator that will wrap any function under @main_wrapper()<br>    def decorator(func: Callable[[AppConfig], None]):<br>        # hydra.main to enable flexible config provisioning<br>        @hydra.main(version_base=None, config_name=&quot;config&quot;)<br>        def _wrapped(cfg: DictConfig):<br>            # important - we&#39;re using dataclasses with additional functionality inside them<br>            # (e.g. methods or __post_init__).<br>            # by default hydra returns an untyped DictConfig.<br>            # with this we convert the config back into a Python dataclass with all methods.<br>            _cfg = OmegaConf.to_container(cfg, structured_config_mode=SCMode.INSTANTIATE)<br>            func(_cfg)<br><br>        return _wrapped<br><br>    return decorator</pre><p>Whoosh, here things get a bit of complex (classics with Python decorators). Let’s schematically describe this logic. What we want is a <em>flexible wrapper</em> for an entrypoint function:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ANAeGy4TUkGRSEEZjW5jlw.png" /><figcaption>self-made in excalidraw.com</figcaption></figure><p>This <em>wrapper</em> should be easy to apply to various entrypoint <strong>functions</strong> (we’ll have two of them — one for table creation, another one for frontend app launch).</p><p>What is the easiest way to add logic to functions in Python? Exactly — <strong>decorators</strong>. Our code would look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0h2YxmG38zhwNJ0yV6Eplg.png" /><figcaption>self-made in excalidraw.com</figcaption></figure><p>A profound reader will also notice that we’ve added a cfg argument to the entrypoints. This is done so we could easily work with the configuration without reading it separately in separate parts of the application.</p><p>One more thing is the way how hydra (and omegaconf which is running under the hood of it) handles the structured configs:</p><pre>@hydra.main(version_base=None, config_name=&quot;config&quot;)<br>        def _wrapped(cfg: DictConfig):<br>           # work with cfg object</pre><p>It actually doesn’t instantiate the dataclass itself, simply providing a dict config after structure validation. To get back the real object with proper methods, a small trick is required:</p><pre>@hydra.main(version_base=None, config_name=&quot;config&quot;)<br>def _wrapped(cfg: DictConfig):<br>    # important - we&#39;re using dataclasses with additional functionality inside them<br>    # (e.g. methods or __post_init__).<br>    # by default hydra returns an untyped DictConfig.<br>    # with this we convert the config back into a Python dataclass with all methods.<br>    _cfg = OmegaConf.to_container(cfg, structured_config_mode=SCMode.INSTANTIATE)<br>    func(_cfg)</pre><p>With all this ✨arcane knowledge✨ we can now wrap this into a nice decorator (code provided above), and use it without worries through the whole application.</p><p>⬆️ <strong>Populating the image metadata table</strong></p><p>It’s time to populate the image metadata table. The code is provided below:</p><pre># db_connect_v2_image_classification/create_table.py<br><br>from databricks.connect.session import DatabricksSession as SparkSession<br>from databricks.sdk.core import Config<br>from pyspark.sql.functions import element_at, split<br><br>from db_connect_v2_image_classification.configs import AppConfig<br>from db_connect_v2_image_classification.main_wrapper import main_wrapper<br><br><br>class CreateTableTask:<br>    def __init__(self, cfg: AppConfig):<br>        self.cfg = cfg<br>        self.spark = SparkSession.builder.sdkConfig(<br>            Config(profile=self.cfg.profile, cluster_id=self.cfg.cluster_id)<br>        ).getOrCreate()<br><br>    def launch(self):<br>        print(f&quot;Loading image metadata into the table {self.cfg.image_table}&quot;)<br>        print(f&quot;Using cluster {self.cfg.cluster_name} with id {self.cfg.cluster_id}&quot;)<br><br>        metadata_df = self._load_image_metadata()<br>        metadata_df.write.saveAsTable(f&quot;{self.cfg.image_table}&quot;, format=&quot;delta&quot;, mode=self.cfg.table_saving_mode)<br>        print(f&quot;Image metadata has been successfully saved into the table {self.cfg.image_table}&quot;)<br><br>    def _load_image_metadata(self):<br>        images = (<br>            self.spark.read.format(&quot;image&quot;)<br>            .load(&quot;/databricks-datasets/flower_photos/*/*.jpg&quot;)<br>            .select(&quot;image.origin&quot;)<br>            .withColumn(&quot;class&quot;, element_at(split(&quot;origin&quot;, &quot;/&quot;), 4))<br>            .withColumn(<br>                &quot;image_id&quot;,<br>                element_at(split(element_at(split(&quot;origin&quot;, &quot;/&quot;), -1), &quot;.jpg&quot;), 1),<br>            )<br>            .select(&quot;image_id&quot;, &quot;class&quot;, &quot;origin&quot;)<br>        )<br>        return images<br><br><br>@main_wrapper()<br>def entrypoint(cfg: AppConfig):<br>    task = CreateTableTask(cfg)<br>    task.launch()</pre><p>It’s a fairly simple piece of Spark code it terms of ETL — we load data from a default dbfs:/databricks-datasets folder (available in every Databricks workspace by default), and simply put the metadata into a separate table:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uyrmTE2zTxlzVH1Yre6fzQ.png" /><figcaption>screenshot of a generated table</figcaption></figure><p>The interesting part is in the way how we initialize the Spark Session. Since we’re using DB Connect “V2”, it’s a fairly simple operation (snippet from the code above):</p><pre>class CreateTableTask:<br>    def __init__(self, cfg: AppConfig):<br>        self.cfg = cfg<br>        self.spark = SparkSession.builder.sdkConfig(<br>            Config(profile=self.cfg.profile, cluster_id=self.cfg.cluster_id)<br>        ).getOrCreate()</pre><p>This initializes a connection where DB Connect “V2” client is running on the local machine, and is capable of sending commands to the remote Databricks cluster.</p><p>🖼️ <strong>Adding a frontend app</strong></p><p>For those who have had a chance to read my <a href="https://medium.com/@polarpersonal/building-a-data-labeling-app-with-python-dash-and-databricks-lakehouse-48341f618b8f">previous blogpost</a>, this part would be pretty straightforward. This part of code initializes from an entrypoint function, passes the config and spins up the Dash app:</p><pre># file: db_connect_v2_image_classification/frontend/app.py<br><br>import logging<br>from random import choice<br><br>from dash import Dash, dcc, html<br><br>from db_connect_v2_image_classification.configs import AppConfig<br>from db_connect_v2_image_classification.frontend.css_utils import external_scripts, external_stylesheets<br>from db_connect_v2_image_classification.frontend.callbacks import prepare_callbacks<br>from db_connect_v2_image_classification.frontend.components import data_container, guideline, header, nav_container<br>from db_connect_v2_image_classification.frontend.crud import DataOperator<br>from db_connect_v2_image_classification.main_wrapper import main_wrapper<br><br>logger = logging.getLogger(&quot;frontend_app&quot;)<br>logger.setLevel(logging.INFO)<br><br><br>def prepare_layout(op: DataOperator) -&gt; html.Div:<br>    logger.info(&quot;Preparing the layout&quot;)<br>    layout = html.Div(<br>        children=[<br>            header,<br>            guideline,<br>            data_container(op.get_all_classes()),<br>            nav_container,<br>            dcc.Store(id=&quot;current_index&quot;, data=choice(op.get_all_indexes())),<br>        ],<br>        className=&quot;p-5&quot;,<br>        style={&quot;height&quot;: &quot;100vh&quot;},<br>    )<br>    logger.info(&quot;layout prepared&quot;)<br>    return layout<br><br><br>@main_wrapper()<br>def entrypoint(cfg: AppConfig):<br>    app = Dash(<br>        __name__,<br>        title=&quot;Image Classification App&quot;,<br>        external_scripts=external_scripts,<br>        external_stylesheets=external_stylesheets,<br>    )<br>    logger.info(&quot;Preparing the data operator&quot;)<br>    op = DataOperator(cfg)<br>    logger.info(&quot;Data operator loaded successfully&quot;)<br>    app.layout = prepare_layout(op)<br>    prepare_callbacks(app, op)<br>    app.run(debug=cfg.debug, host=&quot;0.0.0.0&quot;)<br><br><br>if __name__ == &quot;__main__&quot;:<br>    entrypoint()</pre><p>Line-by-line, the following happens:</p><ol><li>Initialize an empty app object with some extra CSS (I’ve decided to add Tailwind and Daisy UI for some ready-to-use components).</li><li>Prepare the DataOperator instance. I’ve moved all backend-related actions (i.e. read/write ops) into this class for modularity.</li><li>Prepare the layout of the application</li><li>Prepare the callback actions</li><li>Serve the app</li></ol><p>Let’s discuss some of the parts a bit deeper.</p><p>🔛 <strong>Backend interactions</strong></p><p>As said above, the main backbone for R/W operations in this app is DB Connect “V2”. This allows users to write ETL-related code without wrapping it into an ORM-like solutions. For instance, here is the code relevant to the manipulations that could happen in the UI:</p><pre># file: db_connect_v2_image_classification/frontend/crud.py<br><br>import logging<br>from functools import lru_cache<br>from typing import List<br><br>import numpy as np<br>from databricks.connect.session import DatabricksSession as SparkSession<br>from databricks.sdk.core import Config<br>from pyspark.sql import DataFrame<br><br>from db_connect_v2_image_classification.configs import AppConfig<br><br><br>class DataOperator:<br>    def __init__(self, cfg: AppConfig):<br>        self.logger = logging.getLogger(&quot;frontend_app&quot;)<br>        self.cfg = cfg<br>        self.logger.info(&quot;Initializing DB Connect&quot;)<br>        self.spark = SparkSession.builder.sdkConfig(<br>            Config(profile=self.cfg.profile, cluster_id=self.cfg.cluster_id)<br>        ).getOrCreate()<br>        self.logger.info(<br>            f&quot;DB Connect initialized a connection to cluster {self.cfg.cluster_name} with id {self.cfg.cluster_id}&quot;<br>        )<br><br>    @property<br>    def _source_table(self) -&gt; DataFrame:<br>        return self.spark.table(f&quot;{self.cfg.image_table}&quot;)<br><br>    @lru_cache(maxsize=10_000)<br>    def get_all_indexes(self) -&gt; List[str]:<br>        self.logger.info(&quot;Loading all image indexes&quot;)<br>        indexes = self._source_table.select(&quot;image_id&quot;).distinct().toPandas()[&quot;image_id&quot;].to_list()<br>        self.logger.info(&quot;Image indexes loaded&quot;)<br>        return indexes<br><br>    @lru_cache(maxsize=10_000)<br>    def get_all_classes(self) -&gt; List[str]:<br>        return self._source_table.select(&quot;class&quot;).distinct().toPandas()[&quot;class&quot;].to_list()<br><br>    def get_image_class(self, image_id: str) -&gt; str:<br>        return self._source_table.select(&quot;class&quot;).where(f&quot;image_id = &#39;{image_id}&#39;&quot;).toPandas().loc[0, &quot;class&quot;]<br><br>    def get_image_payload(self, image_id: str) -&gt; np.ndarray:<br>        image_origin = self._source_table.select(&quot;origin&quot;).where(f&quot;image_id = &#39;{image_id}&#39;&quot;).toPandas().loc[0, &quot;origin&quot;]<br>        image_info = self.spark.read.format(&quot;image&quot;).load(image_origin).toPandas().T.squeeze()<br><br>        img_payload = np.frombuffer(image_info[&quot;data&quot;], dtype=np.uint8).reshape(<br>            image_info[&quot;height&quot;], image_info[&quot;width&quot;], image_info[&quot;nChannels&quot;]<br>        )[:, :, ::-1]<br><br>        return img_payload<br><br>    def update_image_class(self, image_id: str, new_class: str):<br>        self.logger.info(f&quot;Updating the image class for image {image_id}&quot;)<br>        command = f&quot;UPDATE {self.cfg.image_table} SET class=&#39;{new_class}&#39; WHERE image_id=&#39;{image_id}&#39;&quot;<br>        df = self.spark.sql(command)<br>        df.collect()<br>        self.logger.info(&quot;Update finished&quot;)</pre><p>In the initialization phase we connect to a Databricks cluster via DB Connect, then we can add read-related methods in pure Spark APIs.</p><p>However, some fiddling is required to properly put the binary image data into plotly.imshow :</p><pre>def get_image_payload(self, image_id: str) -&gt; np.ndarray:<br>        image_origin = self._source_table.select(&quot;origin&quot;).where(f&quot;image_id = &#39;{image_id}&#39;&quot;).toPandas().loc[0, &quot;origin&quot;]<br>        image_info = self.spark.read.format(&quot;image&quot;).load(image_origin).toPandas().T.squeeze()<br><br>        img_payload = np.frombuffer(image_info[&quot;data&quot;], dtype=np.uint8).reshape(<br>            image_info[&quot;height&quot;], image_info[&quot;width&quot;], image_info[&quot;nChannels&quot;]<br>        )[:, :, ::-1]<br><br>        return img_payload</pre><p>Line-by-line on what’s happening:</p><ol><li>By running a select on the metadata table, function retrieves the image path (origin) in the cloud storage</li><li>The image information is loaded into a single-element of pd.Series</li><li>The binary data together with height/width and channels information can be used to build a numpy ndarray</li><li>As a last step, this array needs to be reversed to fit the RGB order (this is where we apply [:,:,::-1] operation</li></ol><p>A nice cherry on the cake is that it’s still possible to use old good SQL, e.g. for the UPDATE operations:</p><pre>def update_image_class(self, image_id: str, new_class: str):<br>        self.logger.info(f&quot;Updating the image class for image {image_id}&quot;)<br>        command = f&quot;UPDATE {self.cfg.image_table} SET class=&#39;{new_class}&#39; WHERE image_id=&#39;{image_id}&#39;&quot;<br>        df = self.spark.sql(command)<br>        df.collect()<br>        self.logger.info(&quot;Update finished&quot;)</pre><p>With this class, it’s now fully possible to cover any UI operation. Let’s bind these methods to the UI buttons:</p><pre># file: db_connect_v2_image_classification/frontend/callbacks.py<br><br>from random import choice<br><br>import plotly.express as px<br>from dash import Dash, Input, Output, State<br><br>from db_connect_v2_image_classification.frontend.crud import DataOperator<br><br><br>def prepare_callbacks(app: Dash, op: DataOperator):<br>    @app.callback(<br>        Output(&quot;current_index&quot;, &quot;data&quot;),<br>        Output(&quot;image_id&quot;, &quot;children&quot;),<br>        Output(&quot;class_selector&quot;, &quot;value&quot;),<br>        Output(&quot;img_display&quot;, &quot;figure&quot;),<br>        Input(&quot;random_btn&quot;, &quot;n_clicks&quot;),<br>    )<br>    def get_next_random(_):<br>        next_index = choice(op.get_all_indexes())<br>        img_class = op.get_image_class(next_index)<br>        img_payload = op.get_image_payload(next_index)<br>        figure = px.imshow(img_payload)<br>        figure.update_layout(coloraxis_showscale=False)<br>        figure.update_xaxes(showticklabels=False)<br>        figure.update_yaxes(showticklabels=False)<br>        return next_index, f&quot;Image id: {next_index}&quot;, img_class, figure<br><br>    @app.callback(<br>        Output(&quot;output_mock&quot;, &quot;children&quot;),<br>        Input(&quot;confirm_btn&quot;, &quot;n_clicks&quot;),<br>        State(&quot;class_selector&quot;, &quot;value&quot;),<br>        State(&quot;current_index&quot;, &quot;data&quot;),<br>    )<br>    def save_selected_class(_, value, current_index):<br>        if value:<br>            op.update_image_class(current_index, value)<br>        return value</pre><p>It turns out that the app has a pretty simply logical nature — we only have a random image picker (which will update necessary outputs on each “Next” click), as well as a class saver (which will be triggered on a “Confirm” click.</p><p>🔌 <strong>Adding UI components</strong></p><p>Since the app itself is pretty simple, I’ve only used a couple of nice components and capabilities of Tailwind CSS and Daisy UI:</p><pre># file: db_connect_v2_image_classification/frontend/components.py<br><br>from dash import dcc, html<br><br>blogpost_link = (<br>    &quot;https://www.databricks.com/blog/2022/07/07/introducing-spark-connect-the-power-of-apache-spark-everywhere.html&quot;<br>)<br><br>header = dcc.Markdown(<br>    &quot;## Image classification app, built with [Dash](https://plotly.com/dash/) &quot;<br>    f&quot;and [DB Connect V2]({blogpost_link}) 🔥&quot;,<br>    className=&quot;text-3xl mb-2&quot;,<br>)<br><br><br>guideline = dcc.Markdown(<br>    &quot;Please click below to navigate between images and correct their class labels when required.&quot;,<br>)<br><br>random_btn = html.Button(<br>    &quot;🔀 Next image&quot;,<br>    id=&quot;random_btn&quot;,<br>    n_clicks=0,<br>    className=&quot;btn btn-primary btn-lg mt-5&quot;,<br>)<br><br>nav_container = html.Div(children=[random_btn], className=&quot;flex justify-center&quot;)<br><br>confirm_button = dcc.Loading(<br>    id=&quot;submit-loading&quot;,<br>    children=[<br>        html.Button(<br>            &quot;Confirm&quot;,<br>            id=&quot;confirm_btn&quot;,<br>            n_clicks=0,<br>            className=&quot;btn btn-success btn-block my-4&quot;,<br>        ),<br>        html.Div(id=&quot;output_mock&quot;, style={&quot;display&quot;: &quot;none&quot;}),<br>    ],<br>)<br><br><br>def class_selector(class_choices):<br>    return html.Div(<br>        children=[<br>            html.Div(<br>                children=[<br>                    dcc.Loading(<br>                        children=[<br>                            html.P(&quot;Please choose the class below:&quot;),<br>                            dcc.Dropdown(<br>                                class_choices,<br>                                id=&quot;class_selector&quot;,<br>                                className=&quot;dropdown-content text-black&quot;,<br>                                placeholder=&quot;Select the class&quot;,<br>                                multi=False,<br>                                clearable=False,<br>                            ),<br>                        ]<br>                    )<br>                ],<br>            ),<br>            confirm_button,<br>        ],<br>    )<br><br><br>def data_container(class_choices):<br>    return html.Div(<br>        children=[<br>            html.Div(<br>                className=&quot;flex justify-center pt-2&quot;,<br>                children=[<br>                    html.Div(<br>                        className=&quot;card w-96 bg-base-100 shadow-xl p-4&quot;,<br>                        children=[<br>                            dcc.Loading(<br>                                [<br>                                    dcc.Graph(id=&quot;img_display&quot;),<br>                                    dcc.Markdown(id=&quot;image_id&quot;, className=&quot;text-sm text-sky-500&quot;),<br>                                ]<br>                            ),<br>                            html.Div(className=&quot;divider&quot;),<br>                            class_selector(class_choices),<br>                        ],<br>                    )<br>                ],<br>            ),<br>        ]<br>    )</pre><p>The one I’ve liked the most is the ready-to-use card class with all relevant presets. Take a look at how with just a couple of lines of CSS classes the app could become vivid and UI-friendly:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/683/1*gvTShVbsiO7UiuiNgyonRg.png" /></figure><p>⚙️ <strong>Combining it all together</strong></p><p>Finally, it’s time for a quick demo of the application:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TTlBfUD580QYs_IFbAppDA.gif" /><figcaption>screen-recorded with <a href="https://gifcap.dev/">gifcap.dev</a></figcaption></figure><p>With this app users can easily classify the images, without a need to use an external tool. At the same time, all data is stored in the cloud storage, whilst metadata is saved and updated in a Delta Table.</p><p>✅ <strong>Summary</strong></p><p>With the flexibility of the Dash framework, it’s possible to build modern-looking and flexible frontend applications.</p><p>At the same time, DB Connect works in the backbone of the application, providing convenient Spark APIs to the client-side for proper operations with any kind of data (e.g. images).</p><p>The image data itself can be stored in the cloud storage, whilst the metadata information can be easily retrieved from and updated in the Delta Table.</p><p>All these technologies together provide a powerful toolset for building efficient and modern data applications on top of the Databricks Lakehouse, in pure Python and with the convenience of the well-known APIs.</p><p>The source code from this blog post is provided <a href="https://github.com/renardeinside/db-connect-v2-image-classification">here</a>. Feel free to copy it and fiddle around with your use cases 🙌.</p><p><em>Have you tried using DB Connect “V2” and Dash already? Have an opinion? Feel free to share it in the comments! Also, hit </em><strong><em>subscribe</em></strong><em> if you liked the post — it keeps the author motivated to write more.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3c7f855f6e6" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[On the importance of future-proof data architectures, and how Apache Spark helps to build them]]></title>
            <link>https://polarpersonal.medium.com/on-the-importance-of-future-proof-data-architectures-and-how-apache-spark-helps-to-build-them-9237f8388476?source=rss-5308d3d5c68a------2</link>
            <guid isPermaLink="false">https://medium.com/p/9237f8388476</guid>
            <category><![CDATA[spark]]></category>
            <category><![CDATA[scalability]]></category>
            <category><![CDATA[streaming]]></category>
            <category><![CDATA[data-architecture]]></category>
            <category><![CDATA[domain-driven-design]]></category>
            <dc:creator><![CDATA[Ivan Trusov]]></dc:creator>
            <pubDate>Mon, 10 Apr 2023 21:50:01 GMT</pubDate>
            <atom:updated>2023-04-10T21:56:50.345Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/704/0*le6KFghiH_cBSXmw.jpg" /><figcaption>In the Hold by David Bomberg, 1914. Image source — <a href="https://www.wikiart.org/en/david-bomberg/in-the-hold-1914">Wikiart</a></figcaption></figure><p>In today’s world, we generate and process a vast amount of data every day, and it’s becoming increasingly challenging to manage it all efficiently. Future-proof data architectures that can handle large volumes of data are essential for businesses and organizations.</p><p>In this blog post, I would like to highlight how Apache Spark and its solid set of APIs can ease some technical burdens and make the life of Data Engineers slightly better.</p><p><strong><em>N.B. This post is experimental and doesn’t contain the full code version, merely the snippets and concepts.</em></strong></p><p><strong>The metaphysics of data</strong></p><p>First, we shall consider the concept of data itself and the directions of usage. If you’ll check any relevant source, you’ll find something like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*S_WE7XBGOU3u9zruT-bD_A.png" /><figcaption>self-made in excalidraw.com</figcaption></figure><p>Unfortunately, it’s not provable on a big scale, but I tend to think that the raw data itself is most usually not the main driver of growth in size.</p><p>It grows when it gets <strong>combined</strong> and <strong>reshaped</strong>.</p><p>In my practice I see that the most valuable data is forged out of the following:</p><ul><li><em>Several</em> sources are combined (e.g. enriched or cross-checked)</li><li>Data appears <strong>in time</strong> for the business to make a decision</li><li>Data is properly transformed and quality assured</li></ul><p>On the implementation side, we have pipelines that have a goal to deliver data under the requirements described above.</p><p>And the Data Engineers who are writing these pipelines usually want efficiency in their day-to-day work, for instance:</p><ul><li>Write expressive and efficient code</li><li>Write code that is extensible enough to catch future use cases with ease</li><li>Write code that will be reliable and won’t wake up you in the middle of the night with an alert</li></ul><p>Engineering knowledge tends to be forged and framed out of the fact that people are solving the same problems in different domains — and this is how frameworks are born.</p><p>In the data space, it’s common to use the OSS data processing frameworks (aka ETL frameworks), and one of the most adopted data frameworks in the world is called <a href="https://spark.apache.org/">Apache Spark</a>.</p><p>And I believe amongst other reasons for its <a href="https://star-history.com/#apache/spark&amp;Date">incredible adoption</a>, there are 3 of the most noticeable ones:</p><ul><li><strong>Abstractions <em>when needed</em></strong></li><li><strong>Scalability</strong></li><li><strong>Flexible scheduling</strong></li></ul><p>Combination of which adds the future-proof label to the architecture of data applications. Therefore I would like to highlight these capabilities to the audience.</p><p><strong>Abstractions <em>when needed</em></strong></p><p>Apache Spark allows you to write code without being bound to the specifics of the technical details to a greater extent.</p><p>By this, I mean that it becomes possible to write heavily domain-oriented code with Spark APIs being used when needed, without enforcing users to build everything around the framework.</p><p>Let’s consider the following example: I have a process that reads raw IoT data from a source. I want to apply some changes to the input, join it with some other data and save the result somewhere.</p><p>It is technically <em>possible</em> to make it like this:</p><pre>if __name__ == &quot;__main__&quot;:<br>    raw_iot_events = spark.read.format(&quot;some&quot;).load(source_path)<br>    devices = spark.createDataFrame(pd.read_csv(devices_path))<br><br>    iot_with_devices = raw_iot_events.join(devices, on=&quot;device_id&quot;)<br>    cleaned_iot = iot_with_devices.where(&quot;value &gt; 1&quot;)<br>    cleaned_iot.write.somewhereElse()</pre><p>And this is a valid code. However, it’s not that nice from the perspective of the future, due to several reasons:</p><ul><li>It doesn’t contain any proper domain model</li><li>It doesn’t abstract the physical details</li><li>And there are at least two more issues considered later in this post.</li></ul><p>Let’s rewrite it to something that will be actually more <a href="https://martinfowler.com/bliki/DomainDrivenDesign.html">domain-driven</a>:</p><pre>class Pipe:<br><br>  def source(self, source_definition):<br>    ...<br>  <br>  def destination(self, destination_definition):<br>    ...<br><br>  def join(self, join_rule):<br>    ...<br>  <br>  def transform(self, transformer):<br>    ...<br><br><br>class LaunchScheduler:<br>  ...<br>  <br><br>class IotProcessingTask:<br><br>    def launch(): # application layer - no physical details here<br>        pipe = (<br>            Pipe()<br>                .source(RawEvents(conf.events)) <br>                .source(RawDevices(conf.devices))<br>                .join(EventsAndDevices)<br>                .transform(EventsCleaner)<br>                .destination(CleanEvents)<br>        )<br>        pipe.launch_with(LaunchScheduler(conf.launch))<br><br>if __name__ == &quot;__main__&quot;:<br>    task = IotProcessingTask()<br>    task.launch()</pre><p>I’ve intentionally omitted the implementation details to underline the flexibility of the solution and its independence from the physical implementation (because the <em>framework</em> is the <em>physical</em> detail and not the domain-related one).</p><p>Why do this? Because we want our code to be <strong>properly</strong> <strong>abstracted</strong> for the future (in this context it means stable to the <strong>external</strong> changes).</p><p>Let’s consider that format of the input is an axis of potential changes. In this case, we’ll need simply to change the properties of the configuration, no code changes are required.</p><p>For instance, we can implement a class that is populated from the conf in runtime (it can be even a <a href="https://medium.com/@polarpersonal/quacks-like-a-duck-why-you-probably-should-use-pydantic-more-in-your-python-apps-197accf1fdfc">pydantic</a> BaseModel):</p><pre>class TableCoordinates(BaseModel):<br>  catalog: Optional[str]<br>  database: Optional[str]<br>  table: str<br><br>class RawEvents(BaseModel):<br>  source_type: Literal[&quot;file&quot;, &quot;table&quot;]<br>  source_path: Optional[str]<br>  source_format: Optional[str]<br>  source_table: Optional[TableCoordinates]</pre><p>Into these classes, we can easily pass any kind of payload from a static YAML/JSON/another config format, without a need to change a single line of code:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*M51CRzttyMEXR32K3FYC0w.png" /></figure><p>Another potential axis of change is the logic of how we work within the entity. For example, we got a new field that we need to handle in some specific way:</p><pre>class RawEventsTransformer: # add it as a mixin to the application layer<br>    def transform(df):<br>        return df.withColumn(&quot;event_id&quot;, F.cast(&quot;event_id&quot;, &quot;bigint&quot;))</pre><p>This change is fully isolated from the rest of the codebase, leading us to loosely coupled components that are simple to reuse further.</p><p><strong>Scalability</strong></p><p>Now we’re going into the next dimension of the data pipelines, namely the scalability topic.</p><p>There have been a lot of discussions about using libraries for single-node tasks in the ETL chain. While I appreciate the effort to explore new solutions, I often find myself questioning this approach.</p><p>My reason to worry is that it’s a “working now” solution instead of a future-proof solution.</p><pre># this goes into OOM as soon as source significatly grows in size<br>some_source = single_node_package.read_csv(some_source_path)<br>transformed_in_single_node = some_single_node_transforms(some_source)<br><br>...<br>df_in_spark = spark.createDataFrame(transformed_in_single_node)</pre><p>As I’ve pointed out in the metaphysics part — data has a tendency to grow.</p><p>How soonish could it happen that the some_source the dataset will become out of memory? What would happen if we’ll simply add more rows, or change the logic of the dataset population, e.g. add explicit versioning of the records in this source?</p><p>The solution above will blow out with an OOM, leading developers to incidents and a need to refactor this piece. Indeed there are workarounds, e.g. upscaling the single node to the acceptable RAM amount though. But why would we introduce tech debt that requires workarounds?</p><p>Instead, there is a solution that will be scalable by default:</p><pre>some_source = spark.read.format(&quot;some_format&quot;, some_source_path)<br>transformed_in_distributed = distrubuted_transforms(some_source)</pre><p>And even more, with the <a href="https://spark.apache.org/docs/latest/api/python/user_guide/pandas_on_spark/index.html">Pandas API on Spark</a> transformations in Pandas are available inside Spark itself. Moreover, any code that would accept and produce Pandas DataFrames can be used within Spark itself with <a href="https://www.databricks.com/blog/2020/05/20/new-pandas-udfs-and-python-type-hints-in-the-upcoming-release-of-apache-spark-3-0.html">Pandas UDFs</a>.</p><p><strong>Flexible scheduling</strong></p><p>One important feature of Spark is the flexible API to launch the ETL code in various processing frequency circumstances.</p><p>E.g. you might want to write your code once and make it useful both in <strong>batch</strong> and <strong>streaming</strong>, with the capability to <strong>easily</strong> switch between these behaviors.</p><p>Remember LaunchScheduler from the beginning? Let’s take a closer look at the potential design of this part:</p><pre><br>LaunchScheduler(BaseModel):<br>  once: Optional[bool]<br>  available_now: Optional[bool]<br>  processing_time: Optional[str]<br><br><br>Pipe:<br>  def launch_with(scheduler_config):<br>    # do the basics first<br>    sources = [spark.readStream(source) for source in self.source_definitions]<br>    sources_transformed = [self._transform_source(source) for source in sources]<br>    joined = self._join_with_rule(self.join_rule)<br>    <br>    # process inside batch / micro-batch<br>    def batch_processor(df, epoch_id):<br>       df = self._post_join_transforms(df)<br>       self._write_to_destination(df) # encapsulates writing logic<br><br>    query = (<br>        spark<br>            .foreachBatch(batch_processor)<br>            .trigger(scheduler_config)<br>            .start()<br>    )<br>    query.awaitTermination()</pre><p>The brilliant part comes in the following — it becomes possible to write the ETL code once.</p><p>When a batch is required, it’s enough to useonce or available_now (read <a href="https://spark.apache.org/docs/latest/structured-streaming-programming-guide.html#triggers">here</a> on the differences), and when it comes to a business requirement for faster SLAs, it’s easy to switch to other triggers <strong>without</strong> <strong>changing</strong> the code itself, and by just changing the configuration.</p><p><strong>Summary</strong></p><p>Apache Spark as a framework allows users to build apps that can be easily and efficiently scaled. At the same time code of the data application can be domain-driven and flexibly scheduled without a need to develop two versions of code for batch and streaming use cases.</p><p>These capabilities combined together allow developers to deliver their data applications in a future-proof way without the requirement of costly refactoring in case of changed requirements and data growth.</p><p><em>Have you tried using writing data applications with Apache Spark in a domain-driven design? Have an opinion? Feel free to share it in the comments! Also, hit </em><strong><em>subscribe</em></strong><em> if you liked the post — it keeps the author motivated to write more.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9237f8388476" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building a data labeling app with Python, Dash, and Databricks Lakehouse]]></title>
            <link>https://polarpersonal.medium.com/building-a-data-labeling-app-with-python-dash-and-databricks-lakehouse-48341f618b8f?source=rss-5308d3d5c68a------2</link>
            <guid isPermaLink="false">https://medium.com/p/48341f618b8f</guid>
            <category><![CDATA[dash]]></category>
            <category><![CDATA[sqlalchemy]]></category>
            <category><![CDATA[databricks-sql]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[databricks]]></category>
            <dc:creator><![CDATA[Ivan Trusov]]></dc:creator>
            <pubDate>Wed, 08 Mar 2023 19:11:54 GMT</pubDate>
            <atom:updated>2023-03-08T19:11:54.614Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/737/0*gfrrwsQqOz_k6nJR.jpg" /><figcaption><a href="https://en.wikipedia.org/wiki/File:Charles-Demuth-Sail-In-Two-Movements-1919.jpg">Sail: In Two Movements by Charles Demuth, 1919, Image from Wikimedia Commons</a></figcaption></figure><p>✍️ <strong>Intro</strong></p><p>Data labeling is a crucial aspect of machine learning that involves annotating raw data to create labeled datasets that can be used to train models.</p><p>However, the process of labeling large volumes of data can be time-consuming and error-prone. Fortunately, there are several tools and technologies available that can simplify the data labeling process and increase its efficiency. In this blog post, we will explore how to build a data labeling app using Python, Dash, and the Databricks Lakehouse platform.</p><p>By the end of this post, you will have a better understanding of how to leverage these powerful tools to create a custom data labeling app that can save you time and increase the accuracy of your labeled datasets.</p><p>🏗️ <strong>Architecture</strong></p><p>Let&#39;s take a closer look at the architecture of the labeling app. As usual, we have several layers — frontend, backend, and storage:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*p9Ndsof6vfgn-B4ccHjYwQ.png" /><figcaption>High-level solution architecture</figcaption></figure><p>Some considerations that make this approach distinct from others:</p><ul><li><strong>[Frontend] </strong>Data Scientists usually would prefer to stick to the main language of their choice, which is usually <strong>Python</strong>. Therefore we’ll use <a href="https://plotly.com/dash/">Dash</a> — a robust framework for building UI applications in pure Python.</li><li><strong>[Backend]</strong> This time we’ll use Databricks SQL not as a high-performant query backend (as it’s used in many BI apps), but as a <strong>CRUD</strong> interface to the table with data.</li><li><strong>[Storage]</strong> Since DBSQL is being used and we’ll require Updates, Delta is an obvious choice.</li></ul><p>To sum up — we’re going to build a CRUD-like application with UI on top of the data stored in the Databricks Lakehouse.</p><p><strong>❓What will this app do?</strong></p><p>The app described in the blog post implements a user interface to classify text blocks. Users will be able to open the app in the browser, see the text blocks and select the relevant class per each block. By clicking confirm, the newly selected class will be saved back into the Lakehouse.</p><p>🔷 <strong>Frontend</strong></p><p>First, let’s quickly sketch out the UI component. What we’re going to build is a simple app that allows the user to see some text and then classify it to a given label:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/794/1*k2mS6jvJPlxKFc58qL-lTw.png" /><figcaption>Application UI layout</figcaption></figure><p>Well, maybe it’s not the most enjoyable UI, but definitely a simple and clear one. Hopefully, professional web designers can forgive me for the UX of this one.</p><p><em>If you have ideas on how such an app should look like, drop a line in the comments!</em></p><p>Now it’s time to code a bit. I’ll omit the package setup, take a look at the developer instructions in this repo for details.</p><p>Let’s start by sketching out the layout:</p><pre>app.layout = html.Div(<br>    [<br>        html.Div(<br>            [<br>                header,<br>                guideline,<br>                current_index_view,<br>                html.Div(<br>                    [<br>                        text_container,<br>                        html.Div(<br>                            [<br>                                class_guideline,<br>                                dropdown(CLASSES),<br>                                confirm_button,<br>                            ],<br>                            style={&quot;flex-basis&quot;: &quot;10%&quot;, &quot;padding-left&quot;: &quot;1em&quot;},<br>                        ),<br>                    ],<br>                    style={<br>                        &quot;display&quot;: &quot;flex&quot;,<br>                        &quot;flex-direction&quot;: &quot;row&quot;,<br>                        &quot;padding&quot;: &quot;2em&quot;,<br>                        &quot;margin-bottom&quot;: &quot;2em&quot;,<br>                    },<br>                ),<br>                html.Div(<br>                    navigation_buttons,<br>                    style={<br>                        &quot;justify-content&quot;: &quot;space-evenly&quot;,<br>                        &quot;display&quot;: &quot;flex&quot;,<br>                    },<br>                ),<br>                dcc.Store(id=&quot;current-index&quot;, data=choice(ALL_IDS)),<br>            ],<br>            style={<br>                &quot;padding-top&quot;: &quot;1em&quot;,<br>                &quot;padding-left&quot;: &quot;2em&quot;,<br>                &quot;padding-right&quot;: &quot;2em&quot;,<br>                &quot;padding-bottom&quot;: &quot;5em&quot;,<br>            },<br>        ),<br>    ]<br>)</pre><p>I did some minor changes to the outlook of the original sketch. Let’s see what comes out of it:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*A2wOyd39M_dT8meUG_TLFQ.png" /><figcaption>application UI without additional styling</figcaption></figure><p>Well, this looks kinda functional, but we definitely miss some <strong>💫fancy💫</strong> feeling here.</p><p>Let’s fix these things and sugar-coat it a bit with CSS. We’re going to do several things:</p><ul><li>Add bootstrap to make styles nicer</li><li>Switch to another font</li><li>Add a nice background gradient (because it’s cool and easy!)</li></ul><p>Adding Bootstrap support is pretty easy:</p><pre>external_stylesheets = [<br>    # bootstrap<br>    {<br>        &quot;href&quot;: &quot;https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.min.css&quot;,<br>        &quot;rel&quot;: &quot;stylesheet&quot;,<br>        &quot;integrity&quot;: &quot;sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu&quot;,<br>        &quot;crossorigin&quot;: &quot;anonymous&quot;,<br>    },<br>    # fonts<br>    {&quot;rel&quot;: &quot;preconnect&quot;, &quot;href&quot;: &quot;https://fonts.googleapis.com&quot;},<br>    {<br>        &quot;rel&quot;: &quot;preconnect&quot;,<br>        &quot;href&quot;: &quot;https://fonts.gstatic.com&quot;,<br>        &quot;crossorigin&quot;: &quot;anonymous&quot;,<br>    },<br>    {<br>        &quot;href&quot;: &quot;https://fonts.googleapis.com/css2?family=Inter&amp;family=Noto+Sans&amp;display=swap&quot;,<br>        &quot;rel&quot;: &quot;stylesheet&quot;,<br>    },<br>]<br><br># later in the app - add a reference<br>app = Dash(<br>    __name__, title=&quot;Data Labeling App&quot;, external_stylesheets=external_stylesheets<br>)</pre><p><em>Note: I’m using Bootstrap 3 which is pretty old. Consider using the latest version.</em></p><p>To make buttons look nice, simply add relevant class names to <a href="https://getbootstrap.com/docs/3.3/css/#buttons">them</a>:</p><pre># later in the layout - add some classes to make buttons look better<br>confirm_button = dcc.Loading(<br>    id=&quot;submit-loading&quot;,<br>    children=[<br>        html.Button(<br>            &quot;Confirm&quot;,<br>            id=&quot;confirm_btn&quot;,<br>            n_clicks=0,<br>            className=&quot;btn btn-success btn-block btn-md&quot;,<br>        ),<br>        html.Div(id=&quot;output-mock&quot;, style={&quot;display&quot;: &quot;none&quot;}),<br>    ],<br>)</pre><p>All these steps will make our app look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fy0u3FjkaR1Gd4QW_VviWw.png" /><figcaption>adding bootstrap-like styling</figcaption></figure><p>This already looks softer, but I really don’t like the default fonts of Bootstrap.</p><p>Therefore, let’s also add a new folder called assets and put a file custom.css into it. Please note the package layout:</p><pre>|-- README.md<br>|-- dbsql_labeling_app_example<br>|   |-- __init__.py<br>|   |-- app.py<br>|   |-- assets<br>|   |   `-- custom.css</pre><p>The assets folder <strong>should be in exactly the same folder</strong> as the app.</p><p>With assets configured, put the font settings into the custom.css file:</p><pre>body {<br>    font-family: &#39;Inter&#39;, sans-serif;<br>    height: 100%;<br>}<br><br>html {<br>    height: 100%;<br>}<br><br>h1 {<br>    margin: 0<br>}<br><br>a {<br>    color: plum;<br>}</pre><p>However, the result looks a bit strange — the whole page is out with just one single font. What would be nice is to logically separate the text block that users will classify from the rest of the app.</p><p>This can be achieved by providing a custom font for a component:</p><pre># note the styling part with font-family.<br>dcc.Markdown(<br>                id=&quot;text-container&quot;,<br>                style={<br>                    &quot;font-size&quot;: &quot;1.2em&quot;,<br>                    &quot;font-family&quot;: &quot;&#39;Noto Sans&#39;, sans-serif&quot;,<br>                    &quot;height&quot;: &quot;50vh&quot;,<br>                    &quot;overflow-y&quot;: &quot;auto&quot;,<br>                },<br>            ),</pre><p>All this together will end up in the following UI:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yEt89DZdKzjUxXeD0KpVOA.png" /><figcaption>UI with bootstrap and font styling</figcaption></figure><p>This already looks slicker, but still not <strong>💫fancy💫</strong>. To ensure the proper level of fanciness a nice background gradient is definitely required. I’ve used the <a href="https://mycolor.space/gradient">ColorSpace</a> tool to generate a cool gradient and put it into the body definition:</p><pre>body {<br>    background-image: linear-gradient(to right top, #1b3139, #164472, #754293, #d41677, #ff3621);<br>    color: whitesmoke;<br>    font-family: &#39;Inter&#39;, sans-serif;<br>    height: 100%;<br>}</pre><p>All this together will lead to the following:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rLhgNALt3m-mDvL7YRUbmA.png" /><figcaption>Final UI after styling with CSS and Bootstrap</figcaption></figure><p>The styling is unfortunately only one part of the frontend story. The second one is logic that connects buttons and web components. The logic here is a bit non-linear, therefore writing down the requirements would be helpful.</p><blockquote>The app should be able to show any of the texts provided by backend, together with their associated labels. User should be able to change the label of the text, and should be provided with choices of available classes.</blockquote><p>This description leads to the following:</p><ul><li>A state with an <strong>index</strong> is required. The index will be changed via the buttons and based on the index the text and label will be demonstrated.</li><li>A <strong>variant selector</strong> is required. When the user confirms the choice, the chosen variant should be sent to the backend via update-by-id logic.</li></ul><p>Fortunately, Dash has great capabilities for <a href="https://dash.plotly.com/basic-callbacks#dash-app-with-state">state management</a> and <a href="https://dash.plotly.com/basic-callbacks">callbacks</a>.</p><p>The index part is trivial with the provided capabilities. First, it’s required to define buttons — they’ll will act as inputs:</p><pre>navigation_buttons = [<br>    html.Button(<br>        &quot;⬅️ Previous&quot;,<br>        id=&quot;prev_btn&quot;,<br>        n_clicks=0,<br>        className=&quot;btn btn-default btn-lg&quot;,<br>    ),<br>    html.Button(<br>        &quot;🔀 Random&quot;,<br>        id=&quot;random_btn&quot;,<br>        n_clicks=0,<br>        className=&quot;btn btn-default btn-lg&quot;,<br>    ),<br>    html.Button(<br>        &quot;Next ➡️&quot;,<br>        id=&quot;next_btn&quot;,<br>        n_clicks=0,<br>        className=&quot;btn btn-default btn-lg&quot;,<br>    ),<br>]</pre><p>Second, let’s introduce the index as a <a href="https://dash.plotly.com/dash-core-components/store">store</a> component:</p><pre>dcc.Store(id=&quot;current-index&quot;, data=choice(ALL_IDS)),</pre><p>The Store component allows you to store the data that can be JSON-serialized inside the browser (somewhat similar to React useState).</p><p>Finally, it’s time to bind the buttons to the index. In Dash such interactions are organized as callbacks:</p><pre>@app.callback(<br>    Output(&quot;current-index&quot;, &quot;data&quot;),<br>    Output(&quot;text-container&quot;, &quot;children&quot;),<br>    Output(&quot;class-selector&quot;, &quot;value&quot;),<br>    Output(&quot;prev_btn&quot;, &quot;disabled&quot;),<br>    Output(&quot;next_btn&quot;, &quot;disabled&quot;),<br>    inputs=[<br>        Input(&quot;current-index&quot;, &quot;data&quot;),<br>        Input(&quot;prev_btn&quot;, &quot;n_clicks&quot;),<br>        Input(&quot;next_btn&quot;, &quot;n_clicks&quot;),<br>        Input(&quot;random_btn&quot;, &quot;n_clicks&quot;),<br>    ],<br>)<br>def navigate_to_element(current_index, _, __, ___):<br>    if &quot;prev_btn&quot; == ctx.triggered_id:<br>        current_index -= 1<br>    elif &quot;random_btn&quot; == ctx.triggered_id:<br>        current_index = choice(ALL_IDS)<br>    elif &quot;next_btn&quot; == ctx.triggered_id:<br>        current_index += 1<br><br>    label_data = operator.get_element_by_id(current_index)<br>    disable_previous = current_index &lt;= 0<br>    disable_next = current_index &gt;= len(ALL_IDS) - 1<br>    return (<br>        current_index,<br>        label_data.text,<br>        label_data.label,<br>        disable_previous,<br>        disable_next,<br>    )</pre><p>This specific callback gets triggered in case if any of the navigation buttons are clicked:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DRwT4THn6_TKQ2B0hG8bMA.png" /><figcaption>Navigation control buttons</figcaption></figure><p>The outputs define the side effects of these clicks. As per the code above, there are 5 potential side effects:</p><ul><li>The current index itself will be changed</li><li>Text container component will be filled with text of the relevant sample that is fetched from the Lakehouse</li><li>The label component will be filled with the label of the relevant sample</li><li>Two last outputs will disable Previous/Next buttons if there is no available Id.</li></ul><p>Using the same logic, it’s simple to organize the class choice and confirmation:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/288/1*05gukUKF3hxeMDSYQPZKTA.png" /></figure><p>From the UI perspective, two components are introduced:</p><pre>def dropdown(classes: List[str]) -&gt; html.Div:<br>    return html.Div(<br>        dcc.Loading(<br>            id=&quot;dropdown-loader&quot;,<br>            children=[<br>                dcc.Dropdown(<br>                    classes,<br>                    id=&quot;class-selector&quot;,<br>                    placeholder=&quot;Select the class&quot;,<br>                    multi=False,<br>                    clearable=False,<br>                    style={<br>                        &quot;color&quot;: &quot;black&quot;,<br>                    },<br>                ),<br>            ],<br>        ),<br>        style={<br>            &quot;padding-top&quot;: &quot;10px&quot;,<br>            &quot;padding-bottom&quot;: &quot;10px&quot;,<br>        },<br>    )<br><br><br>confirm_button = dcc.Loading(<br>    id=&quot;submit-loading&quot;,<br>    children=[<br>        html.Button(<br>            &quot;Confirm&quot;,<br>            id=&quot;confirm_btn&quot;,<br>            n_clicks=0,<br>            className=&quot;btn btn-success btn-block btn-md&quot;,<br>        ),<br>        html.Div(id=&quot;output-mock&quot;, style={&quot;display&quot;: &quot;none&quot;}),<br>    ],<br>)</pre><p>And then a callback is added to bind events together:</p><pre>@app.callback(<br>    Output(&quot;output-mock&quot;, &quot;children&quot;),<br>    Input(&quot;confirm_btn&quot;, &quot;n_clicks&quot;),<br>    State(&quot;class-selector&quot;, &quot;value&quot;),<br>    State(&quot;current-index&quot;, &quot;data&quot;),<br>)<br>def save_selected_class(_, value, current_index):<br>    if value:<br>        operator.update_element_by_id(current_index, value)<br>    return value</pre><p>A profound reader might notice above an interesting twist — what’s that output Output(“output-mock”, “children”) is actually used for?</p><p>The explanation is fairly simple — Dash doesn’t support callbacks without outputs.</p><p>At the same time, it’s possible to create a dummy output object that is not visible to users:</p><pre>html.Div(id=&quot;output-mock&quot;, style={&quot;display&quot;: &quot;none&quot;}),</pre><p>Moreover, this object will become pretty useful further when a Loading state will be introduced.</p><p>The loading state handling is a topic that is covered with Dash built-in capabilities completely:</p><pre>text_container = html.Div(<br>    dcc.Loading(<br>        id=&quot;text-block-loading&quot;,<br>        children=[<br>            dcc.Markdown(<br>                id=&quot;text-container&quot;,<br>                style={<br>                    &quot;font-size&quot;: &quot;1.2em&quot;,<br>                    &quot;font-family&quot;: &quot;&#39;Noto Sans&#39;, sans-serif&quot;,<br>                    &quot;height&quot;: &quot;50vh&quot;,<br>                    &quot;overflow-y&quot;: &quot;auto&quot;,<br>                },<br>            ),<br>        ],<br>    ),<br>    style={<br>        &quot;flex-basis&quot;: &quot;90%&quot;,<br>        &quot;padding-right&quot;: &quot;1em&quot;,<br>    },<br>)</pre><p><em>Hint: put a </em><em>html.Div with generic layout styles on top of </em><em>Loading component so it will look stable in the UI.</em></p><p><strong>And this is it</strong> — no need to connect callbacks, if-else, when, etc. Simple and concise wrapper around the UI component that’s being loaded.</p><p>Now to the cool tricks — the output-mock we’ve used above can be used to add a Loading state to the confirm button itself:</p><pre>confirm_button = dcc.Loading(<br>    id=&quot;submit-loading&quot;,<br>    children=[<br>        html.Button(<br>            &quot;Confirm&quot;,<br>            id=&quot;confirm_btn&quot;,<br>            n_clicks=0,<br>            className=&quot;btn btn-success btn-block btn-md&quot;,<br>        ),<br>        html.Div(id=&quot;output-mock&quot;, style={&quot;display&quot;: &quot;none&quot;}),<br>    ],<br>)</pre><p>Since the button is not reloaded when it’s clicked, it’s possible to simply wrap it into a Loading block together with its mocked output. This will add a smooth confirm click behavior:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/925/1*jqvES6M3vxUXsb0hEDNKoA.png" /><figcaption>The logic of binding button and mocked output in Dash</figcaption></figure><p>The app is full of all required utilities for text labeling, and the only thing it awaits is the backend connection to it.</p><p>💎 <strong>Backend</strong></p><p>Per the code above the logic of the backend seems pretty obvious. To simplify the CRUD-related operations I’ve put all of the relevant methods into a single class:</p><pre>from itertools import chain<br>from typing import List, Optional<br><br>from sqlalchemy import select<br>from sqlalchemy.orm import Session<br><br>from dbsql_labeling_app_example.engine import Label, engine<br><br><br>class DataOperator:<br>    def __init__(self) -&gt; None:<br>        print(&quot;Initializing the session for CRUD operator&quot;)<br>        self._session = Session(bind=engine)<br>        print(&quot;Session initialized&quot;)<br><br>    def get_all_ids(self) -&gt; List[int]:<br>        return self._session.execute(select(Label.label_id)).scalars().all()<br><br>    def get_element_by_id(self, id: int) -&gt; Optional[Label]:<br>        return self._session.query(Label).get(id)<br><br>    def update_element_by_id(self, id: int, new_label: str):<br>        self._session.query(Label).filter_by(label_id=id).update(<br>            {Label.label: new_label}<br>        )<br><br>    def get_all_classes(self) -&gt; List[str]:<br>        return list(chain(*self._session.query(Label.label).distinct().all()))</pre><p>This class is used to provide data from the backend towards the frontend app (and therefore used in various callbacks).</p><p>The real beauty of <a href="https://www.sqlalchemy.org/">SQLAlchemy</a> is that all these standard CRUD operations are already in place.</p><p>At the same time, the <a href="https://docs.databricks.com/dev-tools/python-sql-connector.html">Databricks SQL Connector</a> can be easily used together with SQLAlchemy, providing efficient functions and methods to build CRUD operations of any complexity:</p><pre>from sqlalchemy import create_engine<br><br>engine = create_engine(<br>        f&quot;databricks://token:{endpoint_info.token}@{endpoint_info.server_hostname}?http_path={endpoint_info.http_path}&amp;catalog={endpoint_info.catalog}&amp;schema={endpoint_info.database}&quot;,<br>        echo=debug_mode,<br>    )</pre><p>An additional bonus of using SQLAlchemy is that the code becomes well-typed. For instance, the objects that are used in the frontend code, as well as their attributes can be defined in a few lines of code:</p><pre>from sqlalchemy import Column, Integer, String<br>from sqlalchemy.orm import declarative_base<br><br>from dbsql_labeling_app_example.engine_provider import get_prepared_engine<br><br>print(&quot;Preparing the SQL engine&quot;)<br>engine = get_prepared_engine()<br>Base = declarative_base(bind=engine)<br>print(&quot;SQL Engine prepared&quot;)<br><br><br>class Label(Base):<br>    __tablename__ = &quot;labels&quot;<br><br>    label_id = Column(Integer, primary_key=True)<br>    text = Column(String)<br>    label = Column(String)</pre><p>Since the frontend code shares the same codebase and runtime, SerDe between “backend” and “frontend” becomes obsolete.</p><p>✅ <strong>Summary</strong></p><ul><li>CRUD with DBSQL and Delta directly on top of the Lakehouse is a real thing</li><li>With the Dash framework, one can quickly build efficient data applications.</li><li>SQLAlchemy and DBSQL work together pretty well, which allows reusing existing ORM tooling and brings a lot of benefits to Python users.</li></ul><p>The source code for this app as well as demo can be found <a href="https://github.com/renardeinside/dbsql-labeling-app-example">here</a>. Feel free to copy it and fiddle around with your use cases 🙌.</p><p><em>Have you tried using Databricks SQL and Dash already? Have an opinion? Feel free to share it in the comments! Also, hit </em><strong><em>subscribe</em></strong><em> if you liked the post — it keeps the author motivated to write more.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=48341f618b8f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Useful patterns for Scala Spark application development]]></title>
            <link>https://polarpersonal.medium.com/useful-patterns-for-scala-spark-application-development-6799ca2ccb15?source=rss-5308d3d5c68a------2</link>
            <guid isPermaLink="false">https://medium.com/p/6799ca2ccb15</guid>
            <category><![CDATA[app-development]]></category>
            <category><![CDATA[spark]]></category>
            <category><![CDATA[testing]]></category>
            <category><![CDATA[configuration]]></category>
            <category><![CDATA[scala]]></category>
            <dc:creator><![CDATA[Ivan Trusov]]></dc:creator>
            <pubDate>Thu, 26 Jan 2023 22:21:59 GMT</pubDate>
            <atom:updated>2023-01-28T00:06:06.184Z</atom:updated>
            <content:encoded><![CDATA[<h3>Useful patterns for Apache Spark application development with Scala</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/601/0*WQ4Vmqppc4zT2t_b.jpg" /><figcaption><a href="https://www.wikiart.org/en/david-bomberg/ju-jitsu-1913">Ju-Jitsu by David Bomberg, credits to Wikiart</a></figcaption></figure><p>Scala is an incredibly flexible programming language that enabled Software Engineers to build powerful and easily extensible applications in various domains.</p><p>Since a large chunk of Apache Spark is written in Scala, it’s also pretty natural to write Spark applications in Scala language to utilize its typing system fully.</p><p>Although Scala is well-known for its functional libraries (and code written in a pure-functional, monadic style is something that might look unusual for some developers), it also allows users to write in object-oriented style with minor additions of functional programming where needed.</p><p>In this blog post, I wanted to demonstrate some common usage patterns for writing Spark Scala applications in a concise, object-oriented style.</p><p><strong>Using HOCON-based configurations</strong></p><p><a href="https://en.wikipedia.org/wiki/HOCON">HOCON</a> — is a very flexible configuration language, actively used in many Scala-based projects. It supports a pretty wide range of additions, most notable of which I find the following:</p><ul><li>Environment variable substitution</li><li>In-place variable substitution (including nested configs)</li><li>Comments</li><li>Duration and size-like properties, e.g. “10 seconds”</li></ul><p>Here is a simple example of writing and reading such a config:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/caa60114282c0f82f1f51ed5b87205f2/href">https://medium.com/media/caa60114282c0f82f1f51ed5b87205f2/href</a></iframe><p>Reading the config is trivial if we want a standard untyped Config object:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/0fc72a61a50789e81100eaed15d551e1/href">https://medium.com/media/0fc72a61a50789e81100eaed15d551e1/href</a></iframe><p>A nice feature of the HOCON is to share configs across various applications. For example, if you need to configure a chain of tasks, where inputs of one task are dependent on the output configuration of a previous one — HOCON can easily resolve such cases, e.g.:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/1fb1c6333bc70c8cc7746c2843ed41b7/href">https://medium.com/media/1fb1c6333bc70c8cc7746c2843ed41b7/href</a></iframe><p>Another important point of using HOCON is the ability to work nicely with tests. Later in this post, we’ll see how HOCON-based definitions could be injected with testing properties.</p><p><strong>Simplifying tests with testcontainers-scala</strong></p><p>As you can see in the configuration, we’re going to write an application that reads data from a Kafka topic. Therefore, we would need to either mock or provide an example of a Kafka stream in our local environment for proper local tests. Here we can easily use a great JVM-based tool called <a href="https://www.testcontainers.org/">testcontainers</a>, specifically an <a href="https://github.com/testcontainers/testcontainers-scala">extension</a> that makes this library easy to use in Scala.</p><p>By using scalatest and testcontainers developers can easily set up a proper environment for local testing:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/9426dbc2e05ad9b1525aca1aab90ccea/href">https://medium.com/media/9426dbc2e05ad9b1525aca1aab90ccea/href</a></iframe><p>Here is another useful example — this trait provides support for SparkSession with Delta Lake enabled for test cases:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/dbc288f4aa2ff85f4aaab526a8efaba1/href">https://medium.com/media/dbc288f4aa2ff85f4aaab526a8efaba1/href</a></iframe><p>Most certainly an attentive reader has already noticed that spark object is introduced as an <strong><em>implicit</em></strong> here. Therefore let’s talk a little bit about the application layout which makes the setup easier and more concise from the programming perspective.</p><p>The implicit for spark is used for the following reasons:</p><ul><li>It simplifies access to the SparkSessioninstance across the codebase</li><li>Easy injection of this instance across the testing codebase</li></ul><p>Following this idea, one can easily guess that it’s simple to introduce a package-level object which provides access to the SparkSession:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/9180fcdf7aadc6e93d4f0408ba496f94/href">https://medium.com/media/9180fcdf7aadc6e93d4f0408ba496f94/href</a></iframe><p>Now any application inside net.renarde.demos.apps will have access to the Spark Session instance, and in the testing environments a proper local session will be initialized.</p><p><strong>Generic app trait and reading the configs</strong></p><p>Since every application requires a config and a logger instance, a generic app trait can be introduced:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/8522760894ab177c8a96a7903198f245/href">https://medium.com/media/8522760894ab177c8a96a7903198f245/href</a></iframe><p><em>N.B. I’ve omitted </em><em>ConfigProvider code for simplicity, take a look at the </em><a href="https://github.com/renardeinside/dbx-kafka-protobuf-example/tree/main/src/main/scala/net/renarde/dbx/demos/utils"><em>utils</em></a><em> package for more details.</em></p><p>This trait is simple to re-use in various applications, e.g.:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/bf423a360914e7ab661f54ce30dbd2ad/href">https://medium.com/media/bf423a360914e7ab661f54ce30dbd2ad/href</a></iframe><p>By this, we avoid re-writing boilerplate code which reads configs and initializes the logging and SparkSession instance — providing a smooth and fast start to develop a new application.</p><p>Let’s now take a look at the usage of configs. In my example, it’s required to provide KAFKA_BOOTSTRAP_SERVERS environment variable to properly resolve the configuration object. This can be easily done inside tests by overriding the getGlobalConfig function in the ConfigProvider :</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/a0938f98c4f3648963c1ef4d003f130e/href">https://medium.com/media/a0938f98c4f3648963c1ef4d003f130e/href</a></iframe><p>Instead of direct reading from the application.conf (which would be done in case of production launch), the configuration provider function is overridden with properties that are dynamically coming from the test environment configuration and then resolved with a fallback to the main configuration. This approach allows dynamical override and injection of the properties for proper unit testing.</p><p><strong>Some examples and links</strong></p><ul><li><a href="https://github.com/renardeinside/dbx-kafka-protobuf-example">An example project that uses these patterns with Kafka and Protobuf</a></li><li><a href="https://github.com/lightbend/config/blob/main/HOCON.md">HOCON format docs</a></li><li><a href="https://github.com/testcontainers/testcontainers-scala">testcontainers-scala library</a></li></ul><p><em>If you know other useful patterns are there for Spark Scala application development, feel free to share them in the comments!</em></p><p><em>Also, hit </em><strong><em>subscribe</em></strong><em> if you liked the post — it keeps the author motivated to write more.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6799ca2ccb15" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[“Quacks like a duck” — why you probably should use pydantic more in your Python apps]]></title>
            <link>https://polarpersonal.medium.com/quacks-like-a-duck-why-you-probably-should-use-pydantic-more-in-your-python-apps-197accf1fdfc?source=rss-5308d3d5c68a------2</link>
            <guid isPermaLink="false">https://medium.com/p/197accf1fdfc</guid>
            <category><![CDATA[model]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[logic]]></category>
            <category><![CDATA[pydantic]]></category>
            <category><![CDATA[best-practices]]></category>
            <dc:creator><![CDATA[Ivan Trusov]]></dc:creator>
            <pubDate>Thu, 03 Nov 2022 23:06:26 GMT</pubDate>
            <atom:updated>2022-11-03T23:06:26.415Z</atom:updated>
            <content:encoded><![CDATA[<h3>“Quacks like a duck” — why you probably should use pydantic more in Python apps</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/570/0*yO6jeS_Pb5ErQ53E.jpg" /><figcaption>“Bird Seller” by <a href="https://www.wikiart.org/en/vicente-manansala">Vicente Manansala</a>, image credits to <a href="https://www.wikiart.org/en/vicente-manansala/the-bird-seller-1976">WikiArt</a></figcaption></figure><p>⏰ <strong>Borrowing time</strong></p><p>Python is an extremely flexible language. And when I say extremely I sometimes mean it with a little grain of salt. It allows you to write <em>absolutely</em> untyped code without any worries.</p><p>For instance, when you would like to quickly parse an API response and simply extract the necessary results, you might be tempted to do it “quick and dirty”:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/192b8c8999d2e37ac650b018fee4a71e/href">https://medium.com/media/192b8c8999d2e37ac650b018fee4a71e/href</a></iframe><p>And in some cases, it’s a <em>fine solution</em>. Even more, it’s a <strong>valid</strong> Python code in the end. Tools like pylint won’t tell you anything bad about it.</p><p>However anyone who had a chance to write a bigger Python project will have immediate pain while reading this. They just <em>feel</em> that this bit of code will become an imminent source of bugs and issues in the future, especially when the project will start growing.</p><p>Stop for a second and consider — what kind of issues you might face with that code?</p><p>You got it right — literally every line in this code has a potential issue. Let’s describe some of them:</p><ul><li>No guarantees that request will be successful</li><li>No guarantees that request is actually JSON compatible</li><li>No guarantees that response payload will be a dict and not a list</li><li>Even if it’s a dict, there are no guarantees that data key in it</li><li>…</li></ul><p>Same stuff applies to the pretty much every bit of the code above.</p><p>Someone would say — well, we have these guarantees from the API provider, right?</p><p>Yes and no. In fact these guarantees are well provided in case when “everything goes as expected” beyond your application or service boundary. But let’s be honest, this is a too wide statement to be true.</p><p>How could I as a developer regain <em>at least minimal control</em> over the input data, especially when it crosses the service boundary and unexpected structure becomes my problem? If I won’t enforce structures, I might run into:</p><pre>def another_transform_func(inp: Dict[Any, Any]) -&gt; Dict[Any, Any]:</pre><p>Unfortunately my experience shows that using unverified structures on input, as well as Any-&gt;Any-like functions is a way to <strong>borrow time</strong> from future self. It allows you to move fast with the project, but will cost you bug-fixing time later on.</p><p>Is there any way to make it better?</p><p>🦢 <strong>The grace of pydantic</strong></p><p>As a person who developed a lot in Scala, I indeed miss the case classes capabilities.</p><p>And to my opinion one of the best things that happened with Python ecosystem so far is indeed the development and huge adoption of <a href="https://pydantic-docs.helpmanual.io/">pydantic</a>. In fact, I don’t see any reason why it’s still not in the core Python instead of the built-in <a href="https://docs.python.org/3/library/dataclasses.html">@dataclass</a> functionality.</p><p>In the essence, pydantic allows you to create logical models of the entities in your Python application. This could be client/server models, this could be simple logical blocks that serve specific purpose — pretty much anything.</p><p>There are 3 absolutely great features that make pydantic stand out of the line, namely:</p><ul><li><a href="https://pydantic-docs.helpmanual.io/usage/validators/">Validators</a></li><li><a href="https://pydantic-docs.helpmanual.io/usage/types/#nested-discriminated-unions">Nested Discriminated Unions</a></li><li><a href="https://pydantic-docs.helpmanual.io/hypothesis_plugin/">Hypothesis plugin</a></li></ul><p>👀 <strong>Validate ’em all</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rhuqXwuZ5xVahqQPgkKyYw.png" /></figure><p>Validators are essential part of your code with pydantic models, since they allow you to verify the input data straight away at the moment of the object initialization. No need to see NoneType doesn&#39;t have get anymore — it will be covered just at the moment of object initialization.</p><p>Here are some of the nice tricks I use with pydantic:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f40d1f83db3ffec4276ebd44d1ab9203/href">https://medium.com/media/f40d1f83db3ffec4276ebd44d1ab9203/href</a></iframe><p>This validation function allows you to verify that at least one field with specified suffix is provided — e.g. you expect that in the payload you have at least one field that should end with _name suffix. It’s easy to refer to this check in the class:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/4eb4784af680d4f2b5befb04282a78b9/href">https://medium.com/media/4eb4784af680d4f2b5befb04282a78b9/href</a></iframe><p>Another frequently used check is the mutual exclusivity of specific fields:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f6f05ed5b2df7dd6739342f57ce71a7b/href">https://medium.com/media/f6f05ed5b2df7dd6739342f57ce71a7b/href</a></iframe><p>It could be easily applied in the following way:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/0a8db27b6d5b40f6853f9529a8d6fc0c/href">https://medium.com/media/0a8db27b6d5b40f6853f9529a8d6fc0c/href</a></iframe><p>In the end basic validation rules combined with structures could easily cover up to 100% of issues related to incorrectly parsed input data etc.</p><p>What’s more important is that it enforces usage of the logical models further down in your code. It’s way easier to understand a function when it’s clear that it receives one model and returns another one, rather than Any -&gt; Any function.</p><p>🛂 <strong>Divide et impera</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dAXXt3GICn9Krcr3jz7ovw.png" /></figure><p>Another brilliant feature of pydantic is it’s capability to handle nested structures with various objects. It’s a pretty common case when you receive an input that contains various logical objects — and you would like to:</p><ul><li>Identify the object type</li><li>And if the object type is not identifiable — throw an exception.</li></ul><p>The example in the official doc demonstrates quite good the idea of this concept. However, it might be hard to add more custom logic to this identification process, e.g. identify object as A if previous object is B etc. Share your recipes below in the comments for that one!</p><p>🤔 <strong>What-if?</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gg3oYGknKFg1kUiioTK-JA.png" /></figure><p>Another great capability is provided by the hypothesis plugin. Let’s be honest — no one wants to write sample models to test their code.</p><p>It’s just something that takes time and you’re kind of forced to specifically point your samples towards the errors you have (to improve test coverage indeed).</p><p>The hypothesis plugin makes your life slightly easier, and also adds more flexible scenario choices to your tests. Sometimes it helps to catch the “unexpected” errors, whilst manually written samples usually tend to verify the “expected” ones.</p><p>🐳 <strong>Summary</strong></p><p>The necessity of using proper logical models is pretty clear in most of the cases. With the simplicity brought by pydantic, it makes sense to actually start writing new applications from logical models, and only then move to the application logic, organising it as a “dance of entities” (<em>yep, it’s a reference to the </em><a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html"><em>Clean Architecture</em></a>).</p><p>There are some small things that I would like to see better done in pydantic, for instance:</p><ul><li>generated fields straight away in the class definition</li><li>custom functions for discriminated union instead of just property matching</li><li>ability to mixin multiple root validators</li></ul><p>But overall the experience using this library is that it brings efficiency and better code structure, as well as prevents errors.</p><p><em>Did you use pydantic in your projects before? Have an opinion? Feel free to share it in the comments! Also, hit </em><strong><em>subscribe</em></strong><em> if you liked the post — it keeps the author motivated to write more.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=197accf1fdfc" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[End-to-end MLOps with Azure Databricks, Azure AKS and Azure EventHubs. Part #1 — model training]]></title>
            <link>https://polarpersonal.medium.com/end-to-end-mlops-with-azure-databricks-azure-aks-and-azure-eventhubs-part-1-model-training-f191139a36db?source=rss-5308d3d5c68a------2</link>
            <guid isPermaLink="false">https://medium.com/p/f191139a36db</guid>
            <category><![CDATA[hyperparameter-tuning]]></category>
            <category><![CDATA[databricks]]></category>
            <category><![CDATA[mlops]]></category>
            <category><![CDATA[azure]]></category>
            <category><![CDATA[scalability]]></category>
            <dc:creator><![CDATA[Ivan Trusov]]></dc:creator>
            <pubDate>Wed, 10 Aug 2022 08:25:08 GMT</pubDate>
            <atom:updated>2022-08-10T11:05:07.418Z</atom:updated>
            <content:encoded><![CDATA[<h3>End-to-end MLOps with Azure Databricks, Azure AKS and Azure EventHubs. Part #1 — model training</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/594/0*ksKzBJM11a8Lqjqq.jpg" /><figcaption><a href="https://www.wikiart.org/en/giacomo-balla/iridescent-interpenetration-no-7-1912">Iridescent Interpenetration №7 by Giacomo Balla, source: Wikiart</a></figcaption></figure><p>MLOps is a hot topic nowadays. Whilst the importance and business value of it is well described in various papers, e.g. [<a href="https://martinfowler.com/articles/cd4ml.html">1</a>], [<a href="https://proceedings.neurips.cc/paper/2015/file/86df7dcfd896fcaf2674f757a2463eba-Paper.pdf">2</a>], there seems to be a lack of hands-on examples with explanations on how this could be done.</p><p>In this series of blogposts I’m going to demonstrate an example on how to build an ML application following MLOps practices and using Azure Databricks, Azure AKS and Azure EventHubs services. This part is dedicated towards model training and all the small details around it.</p><p><strong>Process overview</strong></p><p>To better understand the process, let’s take a look at this diagram:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*8c7TUg0W7wWYm3BB.png" /><figcaption><a href="https://martinfowler.com/articles/cd4ml.html#IntroductionAndDefinition">The End-to-End CD4ML Process</a></figcaption></figure><p>This diagram tells us that we have three main components of our model that we shall take care of — <strong>code</strong>, <strong>model</strong> and <strong>data</strong>. This is something we’ll take care of with Azure Databricks platform. As for deployment and monitoring, we’ll also use Azure AKS and Azure EventHubs services.</p><p><strong>High-level architecture</strong></p><p>Let’s start from outlining the solution architecture in terms of used technologies and their roles:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AwDlImXYVmmIBwIfTxlSEw.png" /></figure><p>As per MLOps standards, we need to keep jointed versioning of the following parts:</p><ul><li><strong>Data</strong> — versioned in the Lakehouse, since it’s stored in Delta Lake Format with it’s <a href="https://www.databricks.com/blog/2019/02/04/introducing-delta-time-travel-for-large-scale-data-lakes.html">Time Travel</a> capability</li><li><strong>Code</strong> — versioned in GitHub</li><li><strong>Model</strong> — versioned in MLflow</li></ul><p>MLflow also provides a model registry, which allows users to mark specific experiment runs as a model version and easily share the model to the serving layer (Azure AKS).</p><p>As for monitoring, we’re going to serve the model as a ReST API, which means we’ll need to collect the inputs and outputs of the model in real time. A good approach towards this is to send input-output pair from the serving interface directly towards Azure EventHubs, and then collect these metrics into a Lakehouse table. On top of this Lakehouse table we’ll be able to compute and visualize the metrics using DBSQL.</p><p><strong>Preparing the project</strong></p><p>Now it’ s getting to the technical part of implementation, time to roll up the sleeves and write some code. I’m going to use <a href="https://dbx.readthedocs.io/en/latest/">dbx</a> since I prefer to develop ML pipelines in an IDE rather than in a notebook.</p><p>Since IDE is going to be used, some setup on the local machine is required:</p><ol><li>Install <a href="https://docs.conda.io/projects/conda/en/stable/user-guide/install/index.html">conda</a></li><li>Create a new einvironment</li></ol><pre>conda create -n e2e-mlops-demo python=3.9</pre><p>3. Activate a new environment:</p><pre>conda activate e2e-mlops-demo</pre><p>4. Install OpenJDK for local Spark tests:</p><pre>conda install -c conda-forge openjdk=11.0.15</pre><p>5. Install dbx:</p><pre>pip install dbx</pre><p>6. Create a new <a href="https://docs.microsoft.com/en-us/azure/databricks/dev-tools/api/latest/authentication">Databricks API token</a> and configure the CLI:</p><pre>databricks configure --profile e2e-mlops-demo --token</pre><p>7. Check that profile is working correctly:</p><pre>databricks --profile e2e-mlops-demo workspace ls /</pre><p>Now the preparation is done, let’s generate a project skeleton using <a href="https://dbx.readthedocs.io/en/latest/cli.html#dbx-init">dbx init</a>:</p><pre>dbx init -p \<br>    &quot;cicd_tool=None&quot; \<br>    -p &quot;cloud=Azure&quot; \<br>    -p &quot;project_name=e2e-mlops-demo&quot; \<br>    -p &quot;profile=e2e-mlops-demo&quot; \<br>    --no-input</pre><p>This command will generate the project template for us, with some useful settings and assets prepared.</p><p>8. Since I’m using GitHub, it’s easy to create repository from local machine and push it directly to the GitHub without using UI (this command is valid for MacOS and Linux, for PowerShell it needs to be slightly adopted):</p><pre>gh repo create &quot;${PWD##*/}&quot; --private --source=. --remote=origin</pre><p>9. Now it’s time to commit the initial version of the code:</p><pre>git add . # add generated files to repo<br>git commit -m &quot;init&quot;<br>git push -u origin main</pre><p>10. We also cal install and launch the project unit tests:</p><pre>pip install -e &quot;.[dev]&quot;<br>pytest tests/unit --cov</pre><p>I’ve curated the repository slightly (removed some irrelevant files), you can find the relevant version of it here.</p><p><strong>Preparing the data</strong></p><p>Before actually running any kind of a model, we’ll need to prepare some data. For demonstration purposes I’ve chosen the task of credit card fraud classification, and took the public dataset from <a href="https://www.openml.org/search?type=data&amp;sort=runs&amp;id=42175&amp;status=active">OpenML</a>.</p><p>Data is the one of the key parts of the whole MLOps process. On the Databricks Lakehouse platform we indeed store data in Delta Lake — efficient and fully open-sourced format which also supports versioning.</p><p>The code to download data into a Delta table is pretty much trivial (we simply download the DataFrame and do some conversions):</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/bdde48ef9ae193caebb7fa5c59cc6db0/href">https://medium.com/media/bdde48ef9ae193caebb7fa5c59cc6db0/href</a></iframe><p>Main logic is defined in the launch method. We first prepare a separate database, then downloading the data into memory as a Pandas DataFrame, and do some transformations for convenience.</p><p>Final step is to convert this dataset to a Spark DataFrame and then save into a table. This table is also available for other users via SQL statements.</p><p><strong>Data versioning</strong></p><p>Since it’s a Delta Table, it also contains history of all operations:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oB-eF4_6ens7RLXY_-iDfg.png" /><figcaption>Table history in Lakehouse</figcaption></figure><p>We’ll use this feature in our ML training pipeline in the following fashion — when we read the table, we’ll check the latest version of it, and then log this metadata together with our model run into MLflow.</p><p>For better typing I’ve introduced logical models written in <a href="https://pydantic-docs.helpmanual.io/">pydantic</a>:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/fae48067a07553552da0d338e06f7e1b/href">https://medium.com/media/fae48067a07553552da0d338e06f7e1b/href</a></iframe><p>ModelData is the logical class which encapsulates train and test data, as well as providing the source metadata information (version, database and table).</p><p>The source metadata can be easily collected at a time of data reading:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/7094f50a2989bfb9a7189d206e123e98/href">https://medium.com/media/7094f50a2989bfb9a7189d206e123e98/href</a></iframe><p>Using Delta APIs it retrieves the last_version of the dataset, and then using Spark APIs it reads the dataset of a given version into a pandas DataFrame.</p><p><strong>Noticing the imbalance</strong></p><p>When it comes to the data, it’s easy to notice that this dataset could easily fit into memory:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2GCytBhP9KPy9OCbdOnMrQ.png" /></figure><p>It also seems like there is a huge class imbalance — pretty expected situation in fraud detection cases:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZEGqblj-JZhnJMjZMnH3XA.png" /></figure><p>This minimal exploration provides us some valuable info:</p><ul><li>Non-standard metric for the classification model is required</li><li>There is a potential for some over-sampling to balance classes</li><li>There is no need for data or model parallelism, but we could speed-up the hyper-parameters search</li></ul><p><strong><em>Metric choice</em></strong></p><p>Let’s start thinking about the metric first. There are many metrics that support the imbalanced learning, and one of them is Cohen’s Kappa. There are various discussions whenever it’s a best-in-class metric for imbalanced cases.</p><p>I’ve seen this metric being used a couple of times in real-world applications, and it’s already implemented in <a href="https://scikit-learn.org/stable/modules/generated/sklearn.metrics.cohen_kappa_score.html">sklearn</a>. You can read some relevant considerations and discussion <a href="https://stats.stackexchange.com/questions/82162/cohens-kappa-in-plain-english">here</a>.</p><p>We’ll also calculate more standard F1 and ROC AUC scores for better introspection.</p><p><strong><em>Smart over-sampling</em></strong></p><p>To make training process more effective, I’m also going to add some smart oversampling from the great <a href="https://imbalanced-learn.org/stable/">imbalanced-learn</a> library.</p><p>Let’s take a look at the code that is responsible for re-balancing the dataset:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e95948574c1f4b911f2d745a2cefeb22/href">https://medium.com/media/e95948574c1f4b911f2d745a2cefeb22/href</a></iframe><p>Although it looks a bit too verbose from the first sight, it’s not that scary in detail. After trivial split into X and y variables, we first split our data into train and test datasets with stratification by y variable.</p><p>Stratification is important to make sure we preserve the percentage of the target variable in both training and testing datasets.</p><p>Next step is to actually apply over-sampling technique, I’m using the <a href="https://imbalanced-learn.org/stable/references/generated/imblearn.over_sampling.ADASYN.html?highlight=adasyn">ADASYN</a> since it’s fast and pretty robust. Important to mention — we only generate the resampled data for <strong>training</strong> data, and we don’t touch the test to avoid test leakage.</p><p>At this point we’ve covered both topics of metric choice and over-sampling. However, we also have ability to scale out the process of hyper-parameter search. But there are so many scalability patterns, which one is to choose in this specific case?</p><p>Here I make a humble reverence to the scalability in ML tasks.</p><p><strong>Different kinds of scalability</strong></p><p>There are many various ways to scale the ML training process:</p><ul><li>Frameworks that support both data out-of-memory data and can scale with amount of computational resources —for example <a href="https://spark.apache.org/docs/latest/ml-guide.html">SparkML</a>, <a href="https://h2o.ai/">H2O.ai</a>, <a href="https://microsoft.github.io/SynapseML/">SynapseML</a>.</li><li>For deep learning, there are <a href="https://docs.databricks.com/applications/machine-learning/train-model/distributed-training/horovod-runner.html">Horovod</a> + Tensorflow/Pytorch.</li><li>Most-popular boosting algorithms support running via Spark API, providing scalability both in terms of data size and compute(e.g. <a href="https://github.com/microsoft/SynapseML/blob/master/notebooks/features/lightgbm/LightGBM%20-%20Overview.ipynb">LightGBM</a>, <a href="https://www.databricks.com/blog/2020/11/16/how-to-train-xgboost-with-spark.html">XGBoost</a>, <a href="https://catboost.ai/en/docs/installation/spark-installation-pyspark">CatBoost</a>)</li><li>Another option is to parallelise training is to split the dataset horizontally and train a separate model per each partition. Partitioning key could be for example region, or segment. This functionality is available via Pandas UDF functionality and is well described <a href="https://www.databricks.com/blog/2022/07/20/parallel-ml-how-compass-built-a-framework-for-training-many-machine-learning-models-on-databricks.html">here</a>.</li><li>If the dataset fits into memory, its possible to parallelise the process of hyper-parameter search. This functionality is covered by <a href="https://docs.databricks.com/applications/machine-learning/automl-hyperparam-tuning/hyperopt-spark-mlflow-integration.html">hyperopt</a> integration.</li></ul><blockquote>All of these scalability patterns are available in the Databricks Lakehouse Platform.</blockquote><blockquote><em>Side note: check how many of these are available in the typical CDW scenario without exporting data out of it. After thinking about this, take a look at this</em><a href="https://www.linkedin.com/pulse/why-data-lakehouse-best-option-scalable-machine-learning-jason-pohl/?trackingId=vIXeDSbGdCsNMk5Z6C3sqQ%3D%3D"><em> blogpost from Jason Pohl</em></a><em>.</em></blockquote><p><strong>Pipeline and search space</strong></p><p>But let’s get back to the model building topic. For the purpose of this example I’m going to use the latter option and train my model using scikit-learn-based pipeline with hyperopt for hyperparameter search.</p><p>This approach requires to define the search space and the relevant model pipeline. I’ve factored out this code for better readability into a separate class:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/48fec70cc8347979d6478a535a9cf7b0/href">https://medium.com/media/48fec70cc8347979d6478a535a9cf7b0/href</a></iframe><p>As for model, I’m wrapping multiple steps into a chain using Pipeline API from sklearn. This approach unifies the model interface — now it becomes possible to replace the XGBClassifier with any other sklearn-compatible classifier and there is no need to fiddle around training interfaces.</p><p>Search space definition is pretty simple, it’s merely an example of how it could be configured. There is also one interesting thing about hyperopt API logic — after running the <a href="http://hyperopt.github.io/hyperopt/">fmin</a> function it will return a dictionary with a slightly different structure:</p><pre>best_params = fmin(...)<br>print(best_params)</pre><pre># example output:<br>{<br>    &quot;max_depth&quot;: 3, <br>    &quot;n_estimators&quot;: 100,<br>    &quot;learning_rate&quot;: 0.001<br>    &quot;reg_alpha&quot;: 0.03<br>    &quot;base_score&quot;: 0.02<br>}</pre><p>It’s better to be aware of this when applying the best_params object to build the final model.</p><p><strong>Training the model</strong></p><p>With the considerations above it becomes pretty easy to factor out the code for model training into a separate class — Trainer . You can find the full code of this class in <a href="https://github.com/renardeinside/e2e-mlops-demo/tree/v0.0.1">this repo</a>, I’m going to point attention to some details of it.</p><p>To log parameters, metrics and the model in a convenient way a good idea is to create a structure of mlflow runs:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*N6z2fmW4E-4C7beneZBxPQ.png" /></figure><p>In the mlflow UI this structure will look like this, allowing ML engineers to manage and compare the runs in a simple fashion:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Bn8_2aWOfV_U-OfxjXgk0A.png" /></figure><p>I would not recommend saving the model instance of each hyper-parameter run, although it’s possible and might be required in some cases.</p><p>From the API perspective, logging logic will look like this (follow the comments below):</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/a7a8afcb9ce3735454743c1b5355137c/href">https://medium.com/media/a7a8afcb9ce3735454743c1b5355137c/href</a></iframe><ul><li>In the Trainer initialization step the experiment_id and the parent_run_id is obtained. No additional params or metrics are added in the run at this step, this will be done in the end of training process</li><li>The entrypoint method train launches the hyperopt search with the self._objective method as function</li><li>Inside the objective, self.setup_mlflow() method is called. This is only required for local testing, and this method will not have any effect when this code is launched on Databricks. For local testing it’s required to provide the MlflowInfo object during Trainer initialization— it will point the child process spawned by self._objective to use the local MLflow registry.</li><li>The two-levelled with construction in _objective creates a nested run and provides context in which parameters and metrics will be saved by _train_model_and_log_results function.</li><li>After running the fmin function, a dictionary of best_params is generated. At this point the model is re-trained once again with the best_params and then the model instance is saved into the parent run and registered into MLflow Model Registry as a new version.</li><li>Point out the _register_model method which demonstrates how the package code is collected and stored together with the model object. Please note that the code_paths option is available in mlflow&gt;=1.27.0 and on Databricks Runtime 11.0 ML and higher.</li></ul><p>This concludes all the small details around model and code versioning. The main logic of the training is stored inside the _train_model_and_log_results method:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/0baab64690ffe5c597fbdd2d26cbe823/href">https://medium.com/media/0baab64690ffe5c597fbdd2d26cbe823/href</a></iframe><p>This function contains all the logic relevant to actually training and logging the model parameters and metrics. Whilst the pipeline.fit seems pretty standard for ML engineers, here are some additions in this code:</p><ul><li>standard sklearn method get_params() is pretty verbose when it generates parameters dictionary for Pipeline. Therefore I’ve slightly customized it, removing the steps section and auxiliary steps description from it.</li><li>We explicitly save the source information to keep track of the source data version and location</li></ul><p><strong>Local testing</strong></p><p>The provided Trainer class is easily testable on the local machine:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/012fcffadaa93b0ef593bd12153ec154/href">https://medium.com/media/012fcffadaa93b0ef593bd12153ec154/href</a></iframe><p>This test block runs the trainer with provided parameters and fixtures for dataset and mlflow_local instances. In the end it asserts that the model has been created and registered in the local temporary mlflow instance.</p><p>By writing a set of tests, it’s possible to cover the whole data loading and model training process with a decent amount of tests:</p><pre>pytest tests/unit --cov --cov-report=html</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mrxj6-7KaJisSaAMs6-x7g.png" /></figure><p>There is indeed some place for improvement, but as a first shot it’s a good coverage metric.</p><p><strong>Deploying the model to the Model Registry</strong></p><p>The final step for this part is to train the model at scale and make it available in the Model Registry.</p><p>To do this, let’s define a Databricks workflow which consists of two steps:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e24f6997c72f70f4d89773a7ee883429/href">https://medium.com/media/e24f6997c72f70f4d89773a7ee883429/href</a></iframe><p>This deployment file describes all the components we’ll need for this workflow, specifically:</p><ul><li>Cluster definitions</li><li>Task definitions</li><li>Relationship between tasks and clusters</li><li>Order of execution between tasks</li></ul><p>This file follows the Databricks Jobs API 2.1 specification, with some minor alterations that are features of <a href="https://dbx.readthedocs.io/en/latest/pointers/features_pointer.html">dbx</a>, for example:</p><ul><li>instance_pool_name and driver_instance_pool_name will be automatically replaced with their respective IDs</li><li>Locally referenced files (e.g. file:fuse://conf/tasks/builder.yml ) will be automatically uploaded to the DBFS and referenced in the job definition.</li><li>Package containing all the project code will also be automatically built and uploaded to DBFS</li></ul><p>All these actions are to be performed when the following command is launched:</p><pre>dbx deploy --job=e2e-mlops-demo-main</pre><p>After running it, check out the job in Workflows tab:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DAvCKxdJj5moJqk863x6NA.png" /></figure><p>When this job is getting launched, data will be first downloaded and then a new hyperopt search process will be launched. At the end of builder step, a new model version will be created in the MLflow Models Registry. This registry is available via the Models tab in Machine Learning lens:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JMDATR1Cbk6vFBklvZ6q2g.png" /></figure><p>This model version is not yet in any stage, because we haven’t yet defined any stages for it. This is something we’ll do in the next part when the model will be deployed to a serving layer.</p><p><strong>Parameter search scalability</strong></p><p>We all like performance graphs, aren’t we? Here is another one — training a 100 of model evaluations using various amount of worker nodes, specifically 1,2,6,10 and 14 workers configuration.</p><p>Before running the performance tests, some general considerations were taken into account to produce comparable results:</p><ul><li>All tests were performed on Azure Databricks Jobs cluster with Databricks ML Runtime 11.1.x-cpu-ml-scala2.12.</li><li>All cluster nodes (both driver and workers) were pre-warmed to avoid VM startup time effects.</li><li>Average worker node utilization was around 85–90% during the active training phase.</li><li>Since num_estimators was also one of the parameters in XGBoost, it might happen that some tasks were heavier than others. However, most of the training results chose num_estimators in range from 80 to 100.</li></ul><p>You could also read more about some of configuration details below:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sQgUoBhZIZ80R7Ua07h9wQ.png" /></figure><p>The stats for the runs are described below:</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fdatawrapper.dwcdn.net%2FQMpja%2F3%2F&amp;display_name=Datawrapper&amp;url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FQMpja%2F3%2F&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=dwcdn" width="600" height="302" frameborder="0" scrolling="no"><a href="https://medium.com/media/3edff76cd28f8b9904522fc01be2e3a3/href">https://medium.com/media/3edff76cd28f8b9904522fc01be2e3a3/href</a></iframe><p>From these values we could see that in this specific case hyperopt brings pretty good scalability in terms of parallelising the hyper-parameter search process.</p><p><strong>Summary</strong></p><p>In this part of the series the following topics were discussed:</p><ul><li>high-level architecture for MLOps process</li><li>quickstart and project generation</li><li>Various ML training scalability patterns</li><li>Training process with hyperopt</li></ul><p>The code relevant for this version could be found <a href="https://github.com/renardeinside/e2e-mlops-demo/tree/v0.0.1">here</a> (please note the 0.0.1 tag). In the upcoming part we’re going to deep dive into the serving topic. Stay tuned!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f191139a36db" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Releasing and using Python packages with Azure DevOps and Azure Databricks]]></title>
            <link>https://polarpersonal.medium.com/releasing-and-using-python-packages-with-azure-devops-and-azure-databricks-906ec2ff8e2d?source=rss-5308d3d5c68a------2</link>
            <guid isPermaLink="false">https://medium.com/p/906ec2ff8e2d</guid>
            <category><![CDATA[python]]></category>
            <category><![CDATA[azure-devops]]></category>
            <category><![CDATA[packaging]]></category>
            <category><![CDATA[azure]]></category>
            <category><![CDATA[databricks]]></category>
            <dc:creator><![CDATA[Ivan Trusov]]></dc:creator>
            <pubDate>Sun, 19 Jun 2022 09:42:03 GMT</pubDate>
            <atom:updated>2023-03-02T19:00:04.284Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/599/0*YnRwQgluWHGY6T93" /><figcaption><a href="https://commons.wikimedia.org/wiki/File:Hilma_af_Klint_Svanen.jpg">Hilma af Klint “Svanen, nr 17, grupp IX/SUW, serie SUW/UW”, 1915, courtesy to Wikimedia Commons</a></figcaption></figure><p>While working with Python notebooks and projects in a long-term scope, Data and ML engineers come to the idea of using best software development practices in terms of storing and versioning code artifacts.</p><p>One such practice is to organize common, widely re-used code blocks into packages, and store these packages in a reliable system that provides versioning and authentication capabilities.</p><p>In the case of an OSS project in Python, the most obvious choice would be to store your released projects directly in the main Python Package Index (<a href="https://pypi.org/">PyPI</a>). However I doubt that you would like to keep your enterprise code in a public space, so we need to organize a private package index.</p><p>Whilst there are multiple alternatives on how to organize a private package index, I prefer ready-to-use cloud solutions for that. Fortunately, Azure DevOps includes functionality for storing packages, and it’s called <a href="https://docs.microsoft.com/en-us/azure/devops/artifacts/quickstarts/python-packages?view=azure-devops">Azure Artifacts</a>.</p><p>If you’re looking for the source code of the examples used in this blog post, look <a href="https://github.com/renardeinside/databricks-python-packaging">here</a>.</p><p><strong>Architecture</strong></p><p>Here I’ve briefly outlined the solution architecture and the main components:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*p6vEAUJBMqUCShcT8uZWdw.png" /></figure><p>We start from the local repository, and on each push, we run a CI pipeline. When the time for a release comes, we trigger the release one, and it delivers the prepared package to Azure Artifacts.</p><p>On the Databricks side, we connect to the artifact repository (to properly pass auth variables we’ll set up an init script), and then we’re good to install and use our custom package in notebooks and jobs.</p><p><strong>Writing the package</strong></p><p>First, let’s assume that we’re writing a package that contains common Python functions which are used by many downstream teams in our organization. We’ll create a project based on standard Python structure:</p><pre>.<br>|-- README.md<br>|-- azure-pipelines.yml<br>|-- databricks_python_packaging<br>|   |-- __init__.py<br>|   `-- utils<br>|       |-- __init__.py<br>|       `-- common.py<br>|-- notebooks<br>|   `-- usage_example.py<br>|-- pytest.ini<br>|-- scripts<br>|   `-- pip-config.sh<br>|-- setup.py<br>|-- tests<br>|   `-- unit<br>|       |-- conftest.py<br>|       `-- vertical_append_test.py<br>`-- unit-requirements.txt</pre><p>Quick overview of the repo content:</p><ul><li>azure-pipelines.yml contains our CI/CD pipeline definitions</li><li>databricks_python_packaging is a folder with the package source code</li><li>setup.py is a python project file that describes the name, version, and other package properties.</li><li>all other files are relevant for testing and local dependency management</li></ul><p>Assume we currently have one function in our package:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/22374b66b300c79ba4b1685cfb127063/href">https://medium.com/media/22374b66b300c79ba4b1685cfb127063/href</a></iframe><p>It’s a fairly simple function that vertically appends multiple data frames into a single data frame.</p><p>Following software development best practices, we indeed shall also add tests for this function:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e3452037bfeb367e8b364aaf5c918378/href">https://medium.com/media/e3452037bfeb367e8b364aaf5c918378/href</a></iframe><p>I wouldn’t say it’s a good test — it only covers positive cases, but this time it’s not in the focus.</p><p>Now let’s create a new project in Azure DevOps and add the Git Source to it:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jBOz5rEHAVptlYQjp70d1w.png" /></figure><p>In the screen above I’ve created a project databricks-python-packaging and added the GitHub repository with the source code to it.</p><p>I also created a new feed in the Artifacts section, and made it project-scoped.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/842/1*weFg6LUPbTsW8yZClHlffg.png" /></figure><p>The next step is to define the CI/CD pipelines. I’ll only add the relevant parts here, please review the whole file in the linked repository if interested.</p><p>CI pipeline installs dependencies, runs tests, and collects coverage metrics:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/77a7ab275c4f1740838a7f4217a700b3/href">https://medium.com/media/77a7ab275c4f1740838a7f4217a700b3/href</a></iframe><p>And the release pipeline does the most relevant part — builds &amp; uploads the distributed file to the Azure Artifacts feed I created earlier:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/4b918220644ac5420930a849d41e515e/href">https://medium.com/media/4b918220644ac5420930a849d41e515e/href</a></iframe><p>Now the classical steps for a release:</p><ul><li>bump version in __init__.py file</li><li>commit &amp; push code to the main branch</li><li>create &amp; push tag:</li></ul><pre>git tag -a &quot;vX.Y.Z&quot; -m &quot;Release X.Y.Z&quot;<br>git push origin vX.Y.Z</pre><p>As a matter of fact, bumping versions could be automated with tools such as <a href="https://github.com/mtkennerly/dunamai">dunamai</a> or <a href="https://pypi.org/project/versioneer/0.10/">Versioneer</a>, but I’ll keep this automation bit away for the sake of focus.</p><p>After pushing the tag, the release pipeline will do its job and the released package will appear in the Azure Artifacts feed:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-3xH18vUnKex3R2A_w-G6w.png" /></figure><p><strong>Installing package locally</strong></p><p>Imagine now that the developers from one of the teams would like to use this package during the local testing scenario. In my case, it’s a Macbook with Apple M1, so I needed to install .NET SDK via installer and also install dependent packages:</p><pre>pip install keyring artifacts-keyring</pre><p>It’s also required to add an additional package index to the pip.conf file — follow <a href="https://docs.microsoft.com/en-us/azure/devops/artifacts/quickstarts/python-cli?view=azure-devops#consume-python-packages">these instructions</a>.</p><p>When all pre-requisites are done, I can download and install the package via:</p><pre>pip install databricks-python-packaging</pre><p>And use the function I’ve created in my own code.</p><p><strong>Installing package in Azure Databricks</strong></p><p>The final step is to install the package in the Azure Databricks environment. For that we’ll need to use the <a href="https://docs.microsoft.com/en-us/azure/databricks/clusters/init-scripts">cluster node init scripts</a> functionality, to configure the pip.conf file.</p><p>The simplest way to do it is the following:</p><ul><li>Create a new PAT token in Azure DevOps with permissions to read packages:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vlPr2wyWFHtMYLSoHqnLww.png" /></figure><ul><li>Add the following secrets to the AKV in the same way:</li><li>the above-generated token <br>- Azure DevOps feed name<br>- Azure DevOps organization name<br>- If your package is project-scoped (as in this example), add the project name as well.</li></ul><p><em>Please note that this setup expects you to have an attached AKV to the Azure Databricks workspace. Find more on how to organize such a setup </em><a href="https://learn.microsoft.com/en-us/azure/databricks/security/secrets/secret-scopes#--create-an-azure-key-vault-backed-secret-scope"><em>here</em></a><em>.</em></p><blockquote><strong>Security note:</strong> for simplicity and demonstration sake I’m doing the steps above manually. In a full-scale enterprise setup these steps shall be peformed either via Azure ARM or Terraform provider (or any other IaaC solution). Don’t manage your tokens manually and always rotate them.</blockquote><p>On the Databricks side of things, we’ll need to create a new init script which will populate the pip.conf file:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/8fba1612f2e6e0f16ec1e8929ac5708f/href">https://medium.com/media/8fba1612f2e6e0f16ec1e8929ac5708f/href</a></iframe><p>Now we can configure the interactive clusters to use this script and relevant secrets (don’t forget to add the uploaded script to the cluster init scripts configuration):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5LWK-6hXGsXQZmHKzL3FXg.png" /><figcaption>env variables setup</figcaption></figure><p>After this setup, any cluster with the attached init script and provided env variables will be able to install the provided package:</p><pre>%pip install databricks-python-packaging</pre><p>Let’s use our newly packaged function for demonstration purposes:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*foiJeHvdoWy-pSf949-8XA.png" /></figure><p><strong>Summary</strong></p><p>Cloud platforms provide ready-to-use interfaces for software development best practices, although it requires a bit of infrastructure setup.</p><p>For the sake of focus, I’ve intentionally avoided going too deep into infra-level setup (although it’s not the hardest thing to automate). Another simplification step is to add a <a href="https://docs.microsoft.com/en-us/azure/databricks/administration-guide/clusters/policies">cluster policy</a> that automatically propagates the env variables mapping to the cluster definitions.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=906ec2ff8e2d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Writing PySpark logs in Apache Spark and Databricks]]></title>
            <link>https://polarpersonal.medium.com/writing-pyspark-logs-in-apache-spark-and-databricks-8590c28d1d51?source=rss-5308d3d5c68a------2</link>
            <guid isPermaLink="false">https://medium.com/p/8590c28d1d51</guid>
            <category><![CDATA[databricks]]></category>
            <category><![CDATA[apache-spark]]></category>
            <category><![CDATA[logging]]></category>
            <category><![CDATA[logs]]></category>
            <dc:creator><![CDATA[Ivan Trusov]]></dc:creator>
            <pubDate>Mon, 13 Jun 2022 12:55:04 GMT</pubDate>
            <atom:updated>2022-06-13T12:55:04.174Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/489/0*pO1VhweQpCeNYXJv" /><figcaption><a href="https://commons.wikimedia.org/wiki/File:Robert_Delaunay_-_Windows_Open_Simultaneously_(First_Part,_Third_Motif)_-_1912_-_Tate_Modern.jpg">Robert Delaunay — Windows Open Simultaneously, 1912 — Tate Modern, Wikimedia Commons</a></figcaption></figure><p>The closer your data product is getting to the production, the bigger is the importance of properly collecting and analysing logs. Logs help both during debugging in-depth issues and analysing the behaviour of your application.</p><p>For general Python applications the classical choice would be to use the built-in <a href="https://docs.python.org/3/library/logging.html">logging</a> library which has all the necessary components and provides very convenient interfaces for both configuring and working with the logs.</p><p>For PySpark applications, the logging configuration is a little bit more intricate, but still very controllable — it’s just done in a slightly different way, contrary to the classical Python logging.</p><p>In this blogpost I would like to describe approach to effectively create and manage log setup in PySpark applications, both in local environment and on the Databricks clusters.</p><p>If you are looking for the source code, please find it <a href="https://github.com/renardeinside/pyspark-logging-examples">here</a>.</p><p><strong>A little bit of theory</strong></p><p>Apache Spark uses <a href="https://logging.apache.org/log4j/2.x/">log4j</a> package under the hood. Therefore, to properly pass logging messages and unify them across multiple APIs, we’ll need to initialize the logger in a slightly different way, compared to the usual logging package for Python applications.</p><p>In a classical Python environment the logger instance is initialized in the following way:</p><pre>import logging </pre><pre>logger = logging.getLogger(&quot;logger_name&quot;)</pre><p>However for PySpark applications, we’ll need to use the <a href="https://www.py4j.org/">py4j</a> bridge to properly initialize the logger:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e59be8dde4dc37e39c0e8fde49d3aa62/href">https://medium.com/media/e59be8dde4dc37e39c0e8fde49d3aa62/href</a></iframe><p>In this code we do the following:</p><ul><li>Get the LogManager object via py4j bridge</li><li>Obtain the class name with it’s module name and optional custom_prefix</li><li>Return the initialized logger object</li></ul><p>However, simply creating the logger object is not enough, since we also need to provide log4j.properties file. In this file we’ll define the formatting of the logging strings and different levels for packages from which we’re going to collect the logs.</p><p><strong>Apache Spark setup</strong></p><p>While running PySpark applications locally, you most probably are going to install the pyspark package via pip:</p><pre>pip install pyspark</pre><p>And in your application code you most probably are going to initialize the SparkSession object via the following block of code:</p><pre>class SomeApplication:<br>    def __init__(self):<br>        self.spark = SparkSession.builder.getOrCreate()</pre><p>It’s pretty simple to add to this block the logger definition, based on the above-mentioned snippet:</p><pre># located in some_package.some_module</pre><pre>class SampleApp(LoggerProvider):<br>    def __init__(self):<br>        self.spark = SparkSession.builder.getOrCreate()<br>        self.logger = self.get_logger(self.spark)</pre><p>By inheriting the LoggerProvider class, we simplify the process of recognising the SampleApp module name, so in our logs it will be represented as:</p><pre>some_package.some_module.SampleApp &lt;here goes the message&gt;</pre><p>Now to use this logger object we can simply add the following:</p><pre><br>class SampleApp(LoggerProvider):<br>    def __init__(self):<br>        self.spark = SparkSession.builder.getOrCreate()<br>        self.logger = self.get_logger(self.spark)<br>    <br>    def launch(self):<br>        self.logger.debug(&quot;some debugging message&quot;)<br>        self.logger.info(&quot;some info message&quot;)<br>        self.logger.warn(&quot;some warning message&quot;)<br>        self.logger.error(&quot;some error message&quot;)<br>        self.logger.fatal(&quot;some fatal message&quot;)</pre><p>Please note the method names for the self.logger object — they’re <strong>not</strong> the same as a classical logging.Logger API. Since this logger object is based on log4j, it shall follow the log4j methods (e.g. not warning but warn and not critical but fatal).</p><p>If we now launch this application in the local environment, we’ll see that not all messages are actually getting to the output. Also, we might would like to add a bit more detailed formatting to the logs itself:</p><pre>22/06/11 18:39:41 WARN SampleLoggingJob: some warning message<br>22/06/11 18:39:41 ERROR SampleLoggingJob: some error message<br>22/06/11 18:39:41 FATAL SampleLoggingJob: some fatal message</pre><p>Now it’s time to introduce new file — log4j.properties . You can find a reference file <a href="https://github.com/apache/spark/blob/v3.2.1/conf/log4j.properties.template">here</a>. You can easily provide a customized version of this file by using the SPARK_CONF_DIR environment variable as described <a href="https://spark.apache.org/docs/latest/configuration.html#overriding-configuration-directory">here</a>.</p><p>At first, let’s start from something basic:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/a6d0212d13864e9ef97aa3a78ea8f081/href">https://medium.com/media/a6d0212d13864e9ef97aa3a78ea8f081/href</a></iframe><p>If we’ll launch this with our code, we suddenly won’t see any proper logs in the output, but the formatting will be applied properly:</p><pre>[spark][2022-06-13 12:29:26][WARN][org.apache.hadoop.util.NativeCodeLoader][Unable to load native-hadoop library for your platform... using builtin-java classes where applicable]<br>Setting default log level to &quot;WARN&quot;.<br>To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).<br>[spark][2022-06-13 12:29:27][WARN][pyspark_logging_examples.workloads.sample_logging_job.SampleLoggingJob][some warning message]<br>[spark][2022-06-13 12:29:27][ERROR][pyspark_logging_examples.workloads.sample_logging_job.SampleLoggingJob][some error message]<br>[spark][2022-06-13 12:29:27][FATAL][pyspark_logging_examples.workloads.sample_logging_job.SampleLoggingJob][some fatal message]</pre><p>We can also notice this strange line in the log output:</p><pre>Setting default log level to &quot;WARN&quot;.</pre><p>This is where thing get a little bit counterintuitive — although we might expect that the default log level for PySpark applications is taken from log4j.rootCategory=DEBUG, console , it’s actually not exactly true. We’ll need to add <a href="https://books.japila.pl/pyspark-internals/PythonGatewayServer/">another</a> configuration line when we’re running the PySpark application locally:</p><pre>log4j.logger.org.apache.spark.api.python.PythonGatewayServer=DEBUG</pre><p>With this line and some log formatting we’ll have the following file:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/d2a272bf10589eec6f6e9c0bc7729f7c/href">https://medium.com/media/d2a272bf10589eec6f6e9c0bc7729f7c/href</a></iframe><p>And if we launch our application now we’ll get a really rich log output (maybe even too verbose):</p><pre>spark][2022-06-13 12:33:55][DEBUG][org.sparkproject.jetty.util.component.AbstractLifeCycle][starting ServletHandler@694d6976{STOPPED}]<br>[spark][2022-06-13 12:33:55][DEBUG][org.sparkproject.jetty.servlet.ServletHandler][Path=/[EMBEDDED:null] mapped to servlet=org.apache.spark.ui.JettyUtils$$anon$1-42fde1ed[EMBEDDED:null]]<br>[spark][2022-06-13 12:33:55][DEBUG][org.sparkproject.jetty.http.pathmap.PathMappings][Added MappedResource[pathSpec=ServletPathSpec@4e{/},resource=org.apache.spark.ui.JettyUtils$$anon$1-42fde1ed==org.apache.spark.ui.JettyUtils$$anon$1@bace2775{jsp=null,order=-1,inst=false,async=true,src=EMBEDDED:null,STOPPED}] to PathMappings[size=1]]<br>[spark][2022-06-13 12:33:55][DEBUG][org.sparkproject.jetty.servlet.ServletHandler][filterNameMap={org.apache.spark.ui.HttpSecurityFilter-566be2b0=org.apache.spark.ui.HttpSecurityFilter-566be2b0==org.apache.spark.ui.HttpSecurityFilter@566be2b0{inst=false,async=true,src=EMBEDDED:null}}]<br>[spark][2022-06-13 12:33:55][DEBUG][org.sparkproject.jetty.servlet.ServletHandler][pathFilters=[[/*]/[]/[INCLUDE, REQUEST, FORWARD, ERROR, ASYNC]=&gt;org.apache.spark.ui.HttpSecurityFilter-566be2b0]]<br>[spark][2022-06-13 12:33:55][DEBUG][org.sparkproject.jetty.servlet.ServletHandler][servletFilterMap={}]<br>[spark][2022-06-13 12:33:55][DEBUG][org.sparkproject.jetty.servlet.ServletHandler][servletPathMap=PathMappings[size=1]]<br>[spark][2022-06-13 12:33:55][DEBUG][org.sparkproject.jetty.servlet.ServletHandler][servletNameMap={org.apache.spark.ui.JettyUtils$$anon$1-42fde1ed=org.apache.spark.ui.JettyUtils$$anon$1-42fde1ed==org.apache.spark.ui.JettyUtils$$anon$1@bace2775{jsp=null,order=-1,inst=false,async=true,src=EMBEDDED:null,STOPPED}}]<br>[spark][2022-06-13 12:33:55][DEBUG][org.sparkproject.jetty.server.handler.AbstractHandler][starting ServletHandler@694d6976{STARTING}]<br>[spark][2022-06-13 12:33:55][DEBUG][org.sparkproject.jetty.util.component.AbstractLifeCycle][STARTED <a href="http://twitter.com/2251ms">@2251ms</a> ServletHandler@694d6976{STARTED}]</pre><p>Now it’s time to actually configure it properly, so we see only relevant messages from our application and suppress too much detailed logging information. In this example we follow the log4j.properties.template with some minor adjustments:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/199656c63501b1c8101624b881d6730c/href">https://medium.com/media/199656c63501b1c8101624b881d6730c/href</a></iframe><p>The logs look now way less verbose, but it seems to me that I’ve lost the debugging details from my application:</p><pre>[spark][2022-06-13 12:39:06][INFO][pyspark_logging_examples.workloads.sample_logging_job.SampleLoggingJob][some info message]<br>[spark][2022-06-13 12:39:06][WARN][pyspark_logging_examples.workloads.sample_logging_job.SampleLoggingJob][some warning message]<br>[spark][2022-06-13 12:39:06][ERROR][pyspark_logging_examples.workloads.sample_logging_job.SampleLoggingJob][some error message]<br>[spark][2022-06-13 12:39:06][FATAL][pyspark_logging_examples.workloads.sample_logging_job.SampleLoggingJob][some fatal message]</pre><p>This output is missing the debugging commands I’ve had in my application — and it makes sense since I’ve set our root and GatewayServer configuration to INFO level. Here the log4j configuration flexibility comes in handy. Since I know that the name of my package is pyspark_logging_examples — I can adjust my logging output based on it’s name by simply adding one line:</p><pre># Custom line for my Python packagelog4j.logger.pyspark_logging_examples=DEBUG</pre><p>Now we’re talking — all logs are properly printed:</p><pre>[spark][2022-06-13 12:45:20][DEBUG][pyspark_logging_examples.workloads.sample_logging_job.SampleLoggingJob][some debugging message]<br>[spark][2022-06-13 12:45:20][INFO][pyspark_logging_examples.workloads.sample_logging_job.SampleLoggingJob][some info message]<br>[spark][2022-06-13 12:45:20][WARN][pyspark_logging_examples.workloads.sample_logging_job.SampleLoggingJob][some warning message]<br>[spark][2022-06-13 12:45:20][ERROR][pyspark_logging_examples.workloads.sample_logging_job.SampleLoggingJob][some error message]<br>[spark][2022-06-13 12:45:20][FATAL][pyspark_logging_examples.workloads.sample_logging_job.SampleLoggingJob][some fatal message]</pre><p>In the same manner you can mute (or add) more detailed logs per different components of Apache Spark — just notice the ones that are too verbose and point them to a higher level, e.g. if I think that the DAG scheduler logs are too verbose on the INFO level, I can simply cap them to WARN level:</p><pre>log4j.logger.org.apache.spark.scheduler.DAGScheduler=WARN</pre><p>Summarizing on this path, here are some important things to consider when driving PySpark application logs in the OSS Apache Spark environment:</p><ul><li>Use the Spark-based logger and not the logging-based one</li><li>Provide your logging configurations in log4j.properties file and place it in the SPARK_CONF_DIR path</li><li>Don’t forget to keep in sync the rootCategory and PythonGatewayServer logging properties</li><li>You can easily customize your logging levels per different loggers (which could be your application or specific Apache Spark class or package)</li></ul><p><strong>Managing logging configurations in the Databricks environment</strong></p><p>On the Databricks platform, we’ll reuse the above mentioned class called <a href="https://github.com/renardeinside/pyspark-logging-examples/blob/main/notebooks/sample-logs-collection.py">LoggerProvider</a> to write the logs:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ePAGZhg1BQR5V-wsO2rfnQ.png" /></figure><p>After running this code, in the Driver Logs tab, in the log4j output we’ll see the following lines:</p><pre>22/06/13 11:20:00 INFO LoggerProvider: some info message<br>22/06/13 11:20:00 WARN LoggerProvider: some warning message<br>22/06/13 11:20:00 FATAL LoggerProvider: some fatal message</pre><p>However we also would like to slightly customize the logging output (add more formatting and add the fully qualified class names to it). We can easily do this by using the cluster node initialization script as described <a href="https://kb.databricks.com/clusters/overwrite-log4j-logs.html">here</a>.</p><p>We’ll need to change the layout of the publicFile appender to the following:</p><pre>log4j.appender.publicFile.layout.ConversionPattern=[spark][%p][%d{yy/MM/dd HH:mm:ss}][%c][%m]%n</pre><p>And now in the log4j section we see pretty clean log output:</p><pre>[spark][INFO][22/06/13 11:56:01][notebooks.__main__.LoggerProvider][some info message]<br>[spark][WARN][22/06/13 11:56:01][notebooks.__main__.LoggerProvider][some warning message]<br>[spark][FATAL][22/06/13 11:56:01][notebooks.__main__.LoggerProvider][some fatal message]</pre><p>however we might have a lot of different notebooks using the same cluster at a time, so it would be great to properly prepend the path to uniquely distinguish different logs:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*D-KKabft-O6mG4RofHN-Bg.png" /></figure><p>And now our logs provide full visibility on the path on the source notebook:</p><pre>[spark][INFO][22/06/13 12:03:32][notebooks.some.path.sample-logs-collection.__main__.LoggerProvider][some info message]<br>[spark][WARN][22/06/13 12:03:32][notebooks.some.path.sample-logs-collection.__main__.LoggerProvider][some warning message]<br>[spark][FATAL][22/06/13 12:03:32][notebooks.some.path.sample-logs-collection.__main__.LoggerProvider][some fatal message]</pre><p>By further customizing the log4j.properties you can suppress specific components and classes from the logs (or enable a more in-depth debugging for specific parts).</p><p>You can find all the above mentioned code pieces and more profound example in <a href="https://github.com/renardeinside/pyspark-logging-examples">this</a> repository.</p><p>In this post I’ve tried to cover different options for configuring log4j and using it as a main logging engine for a PySpark application both in OSS Apache Spark and Databricks context. The one topic which remained not properly covered is the setup, configuration and collection of the logs on the worker nodes.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8590c28d1d51" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>