<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>python Archives - OVHcloud Blog</title>
	<atom:link href="https://blog.ovhcloud.com/tag/python/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.ovhcloud.com/tag/python/</link>
	<description>Innovation for Freedom</description>
	<lastBuildDate>Fri, 03 Mar 2023 16:40:25 +0000</lastBuildDate>
	<language>en-GB</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://blog.ovhcloud.com/wp-content/uploads/2019/07/cropped-cropped-nouveau-logo-ovh-rebranding-32x32.gif</url>
	<title>python Archives - OVHcloud Blog</title>
	<link>https://blog.ovhcloud.com/tag/python/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Deploy a custom Docker image for Data Science project – A spam classifier with FastAPI (Part 3)</title>
		<link>https://blog.ovhcloud.com/deploy-a-custom-docker-image-for-data-science-project-a-spam-classifier-with-fastapi-part-3/</link>
		
		<dc:creator><![CDATA[Eléa Petton]]></dc:creator>
		<pubDate>Fri, 30 Dec 2022 10:39:54 +0000</pubDate>
				<category><![CDATA[OVHcloud Engineering]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[AI Deploy]]></category>
		<category><![CDATA[AI Notebook]]></category>
		<category><![CDATA[AI Solutions]]></category>
		<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[Machine learning]]></category>
		<category><![CDATA[OVHcloud]]></category>
		<category><![CDATA[python]]></category>
		<category><![CDATA[Scikit Learn]]></category>
		<category><![CDATA[spam classification]]></category>
		<guid isPermaLink="false">https://blog.ovhcloud.com/?p=24202</guid>

					<description><![CDATA[A guide to deploy a custom Docker image for an API with FastAPI and AI Deploy. Welcome to the third article concerning custom Docker image deployment. If you haven&#8217;t read the previous ones, you can check it: &#8211; Gradio sketch recognition app&#8211; Streamlit app for EDA and interactive prediction When creating code for a Data [&#8230;]<img src="//blog.ovhcloud.com/wp-content/plugins/matomo/app/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fblog.ovhcloud.com%2Fdeploy-a-custom-docker-image-for-data-science-project-a-spam-classifier-with-fastapi-part-3%2F&amp;action_name=Deploy%20a%20custom%20Docker%20image%20for%20Data%20Science%20project%20%E2%80%93%20A%20spam%20classifier%20with%20FastAPI%20%28Part%203%29&amp;urlref=https%3A%2F%2Fblog.ovhcloud.com%2Ffeed%2F" style="border:0;width:0;height:0" width="0" height="0" alt="" />]]></description>
										<content:encoded><![CDATA[
<p><em>A guide to deploy a custom Docker image for an API with <a href="https://fastapi.tiangolo.com/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">FastAPI</a> and <strong>AI Deploy</strong>.</em></p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="815" src="https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-spam-classifier-1024x815.jpg" alt="fastapi for spam classification" class="wp-image-24226" srcset="https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-spam-classifier-1024x815.jpg 1024w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-spam-classifier-300x239.jpg 300w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-spam-classifier-768x612.jpg 768w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-spam-classifier-1536x1223.jpg 1536w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-spam-classifier.jpg 1620w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p><em>Welcome to the third article concerning <strong>custom Docker image deployment</strong>. If you haven&#8217;t read the previous ones, you can check it:</em></p>



<p><em>&#8211; </em><a href="https://blog.ovhcloud.com/deploy-a-custom-docker-image-for-data-science-project-gradio-sketch-recognition-app-part-1/" data-wpel-link="internal">Gradio sketch recognition app</a><br><em>&#8211; </em><a href="https://docs.ovh.com/fr/publiccloud/ai/deploy/tuto-streamlit-eda-iris/" data-wpel-link="exclude">Streamlit app for EDA and interactive prediction</a></p>



<p>When creating code for a <strong>Data Science project</strong>, you probably want it to be as portable as possible. In other words, it can be run as many times as you like, even on different machines.</p>



<p>Unfortunately, it is often the case that a Data Science code works fine locally on a machine but gives errors during runtime. It can be due to different versions of libraries installed on the host machine.</p>



<p>To deal with this problem, you can use <a href="https://www.docker.com/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Docker</a>.</p>



<p><strong>The article is organized as follows:</strong></p>



<ul class="wp-block-list">
<li>Objectives</li>



<li>Concepts</li>



<li>Define a model for spam classification</li>



<li>Build the FastAPI app with Python</li>



<li>Containerize your app with Docker</li>



<li>Launch the app with AI Deploy</li>
</ul>



<p><em>All the code for this blogpost is available in our dedicated <a href="https://github.com/ovh/ai-training-examples/tree/main/apps/fastapi/spam-classifier-api" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">GitHub repository</a>. You can test it with OVHcloud <strong>AI Deploy</strong> tool, please refer to the <a href="https://docs.ovh.com/gb/en/publiccloud/ai/deploy/tuto-fastapi-spam-classifier/" data-wpel-link="exclude">documentation</a> to boot it up.</em></p>



<h2 class="wp-block-heading">Objectives</h2>



<p>In this article, you will learn how to develop <strong>FastAPI</strong> API for spam classification.</p>



<p>Once your app is up and running locally, it will be a matter of containerizing it, then deploying the custom Docker image with AI Deploy.</p>



<figure class="wp-block-image size-large"><img decoding="async" width="2160" height="1215" src="https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-objective-edited.jpg" alt="objective of api deployment" class="wp-image-24228" srcset="https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-objective-edited.jpg 2160w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-objective-edited-300x169.jpg 300w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-objective-edited-1024x576.jpg 1024w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-objective-edited-768x432.jpg 768w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-objective-edited-1536x864.jpg 1536w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-objective-edited-2048x1152.jpg 2048w" sizes="(max-width: 2160px) 100vw, 2160px" /></figure>



<h2 class="wp-block-heading">Concepts</h2>



<p>In Artificial Intelligence, you have probably heard of <strong>Natural Language Processing</strong> (NLP). <strong>NLP</strong> gathers several tasks related to language processing such as <strong>text classification</strong>.</p>



<p>This technique is ideal for distinguishing spam from other messages.</p>



<h3 class="wp-block-heading">Spam Ham Collection&nbsp;Dataset</h3>



<p>The <a href="https://archive.ics.uci.edu/ml/datasets/sms+spam+collection" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">SMS Spam Collection</a> is a public set of SMS labeled messages that have been collected for mobile phone spam research.</p>



<p>The dataset contains <strong>5,574 messages</strong> in English. The SMS are tagged as follow:</p>



<ul class="wp-block-list">
<li><strong>HAM</strong> if the message is legitimate</li>



<li><strong>SPAM</strong> if it is not</li>
</ul>



<p>The collection is a <strong>text file</strong>, where each line has the correct <strong>class</strong> followed by the raw <strong>message</strong>.</p>



<figure class="wp-block-image aligncenter size-large is-resized"><img decoding="async" src="https://blog.ovhcloud.com/wp-content/uploads/2022/12/image-5-1024x576.png" alt="spam ham dataset" class="wp-image-24219" width="773" height="435" srcset="https://blog.ovhcloud.com/wp-content/uploads/2022/12/image-5-1024x576.png 1024w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/image-5-300x169.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/image-5-768x432.png 768w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/image-5-1536x864.png 1536w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/image-5.png 1920w" sizes="(max-width: 773px) 100vw, 773px" /></figure>



<h3 class="wp-block-heading">Logistic regression</h3>



<p><strong>What is a Logistic Regression?</strong></p>



<p><a href="https://fr.wikipedia.org/wiki/R%C3%A9gression_logistique" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Logistic regression</a> is a statistical model. It allows to study the relationships between a set of <code>i</code> <strong>qualitative variables</strong> (<code>Xi</code>) and a <strong>qualitative variable</strong> (<code>Y</code>).</p>



<figure class="wp-block-image aligncenter size-large is-resized"><img loading="lazy" decoding="async" src="https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-logistic-regression-1024x779.jpg" alt="logistic regression" class="wp-image-24229" width="467" height="355" srcset="https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-logistic-regression-1024x779.jpg 1024w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-logistic-regression-300x228.jpg 300w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-logistic-regression-768x584.jpg 768w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-logistic-regression-1536x1168.jpg 1536w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-logistic-regression.jpg 1620w" sizes="auto, (max-width: 467px) 100vw, 467px" /></figure>



<p>It is a generalized linear model using a logistic function as a link function.</p>



<p>A logistic regression model can also predict the <strong>probability</strong> of an event occurring (value close to <code><strong>1</strong></code>) or not (value close to <strong><code>0</code></strong>) from the optimization of the <strong>regression coefficients</strong>. This result always varies between <strong><code>0</code></strong> and <strong><code>1</code></strong>.</p>



<p>For the spam classification use case, <strong>words</strong> are inputs and <strong>class</strong> (spam or ham) is output.</p>



<h3 class="wp-block-heading">FastAPI</h3>



<p><strong>What is FastAPI?</strong></p>



<p><a href="https://fastapi.tiangolo.com/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">FastAPI</a> is a web framework for building <strong>RESTful APIs</strong> with Python.</p>



<p>FastAPI is based on <a href="https://docs.pydantic.dev/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Pydantic</a> and type guidance to <em>validate</em>, <em>serialize</em> and <em>deserialize</em> data, and automatically generate OpenAPI documents.</p>



<h3 class="wp-block-heading">Docker</h3>



<p><a href="https://www.docker.com/" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Docker</a>&nbsp;platform allows you to build, run and manage isolated applications. The principle is to build an application that contains not only the written code but also all the context to run the code: libraries and their versions for example</p>



<p>When you wrap your application with all its context, you build a Docker image, which can be saved in your local repository or in the Docker Hub.</p>



<p>To get started with Docker, please, check this&nbsp;<a href="https://www.docker.com/get-started" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">documentation</a>.</p>



<p>To build a Docker image, you will define 2 elements:</p>



<ul class="wp-block-list">
<li>the application code (<em>FastAPI app</em>)</li>



<li>the&nbsp;<a href="https://docs.docker.com/engine/reference/builder/" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Dockerfile</a></li>
</ul>



<p>In the next steps, you will see how to develop the Python code for your app, but also how to write the Dockerfile.</p>



<p>Finally, you will see how to deploy your custom docker image with&nbsp;<strong>OVHcloud AI Deploy</strong>&nbsp;tool.</p>



<h3 class="wp-block-heading">AI Deploy</h3>



<p><strong>AI Deploy</strong>&nbsp;enables AI models and managed applications to be started via Docker containers.</p>



<p>To know more about AI Deploy, please refer to this&nbsp;<a href="https://docs.ovh.com/gb/en/publiccloud/ai/deploy/getting-started/" data-wpel-link="exclude">documentation</a>.</p>



<h2 class="wp-block-heading">Define a model for spam classification</h2>



<p>❗ <strong><code>To develop an API that uses a Machine Learning model, you have to load the model in the correct format. For this tutorial, a Logistic Regression is used and the Python file model.py is used to define it</code></strong>.<br><br><code><strong>To better understand the model.py code, refer to the <a href="https://github.com/ovh/ai-training-examples/blob/main/notebooks/natural-language-processing/text-classification/miniconda/spam-classifier/notebook-spam-classifier.ipynb" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">notebook</a> which details all the steps</strong></code>.</p>



<p>First of all, you have to import the&nbsp;<strong>Python libraries</strong>&nbsp;needed to create the Logistic Regression in the <code>model.py</code> file.</p>



<pre class="wp-block-code"><code class="">import pandas as pd
import numpy as np
from sklearn import model_selection
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression</code></pre>



<p>Now, you can create the Logistic Regression based on the <strong>Spam Ham Collection&nbsp;Dataset</strong>. The Python framework named <strong>Scikit-Learn</strong> is used to define this model.</p>



<p>Firstly, you can load the dataset and transform your input file into a <code>dataframe</code>.</p>



<p>You will also be able to define the <code>input</code> and the <code>output</code> of the model.</p>



<pre class="wp-block-code"><code class="">def load_data():

    PATH = 'SMSSpamCollection'
    df = pd.read_csv(PATH, delimiter = "\t", names=["classe", "message"])

    X = df['message']
    y = df['classe']

    return X, y</code></pre>



<p>In a second step, you split the data in a training and a test set.</p>



<p>To <strong>separate the dataset fairly</strong> and to have a <code>test_size</code> between 0 and 1, you can calculate <code>ntest</code> as follows.</p>



<pre class="wp-block-code"><code class="">def split_data(X, y):

    ntest = 2000/(3572+2000)

    X_train, X_test, y_train, y_test = model_selection.train_test_split(X, y, test_size=ntest, random_state=0)

    return X_train, y_train</code></pre>



<p>Now you can concentrate on creating the <strong>Machine Learning model</strong>. To do this, create a <code>spam_classifier_model</code> function.</p>



<p>To fully understand the code, refer to <strong>Steps 6 to 9</strong> of this <a href="https://github.com/ovh/ai-training-examples/blob/main/notebooks/natural-language-processing/text-classification/miniconda/spam-classifier/notebook-spam-classifier.ipynb" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">notebook</a>. In these steps you will learn how to:</p>



<ul class="wp-block-list">
<li>create the model using <strong>Logistic Regression</strong></li>



<li>evaluate on the test set</li>



<li>do <strong>dimension reduction</strong> with stop words and term frequency</li>



<li>do <strong>dimension reduction</strong> to post-processing of the model</li>
</ul>



<pre class="wp-block-code"><code class="">def spam_classifier_model(Xtrain, ytrain):

    model_logistic_regression = LogisticRegression()
    model_logistic_regression = model_logistic_regression.fit(Xtrain, ytrain)

    coeff = model_logistic_regression.coef_
    coef_abs = np.abs(coeff)

    quantiles = np.quantile(coef_abs,[0, 0.25, 0.5, 0.75, 0.9, 1])

    index = np.where(coeff[0] &gt; quantiles[1])
    newXtrain = Xtrain[:, index[0]]

    model_logistic_regression = LogisticRegression()
    model_logistic_regression.fit(newXtrain, ytrain)

    return model_logistic_regression, index</code></pre>



<p>Once these Python functions are defined, you can call and apply them as follows.</p>



<p>Firstly, extract input and output data with <code>load_data()</code>:</p>



<pre class="wp-block-code"><code class="">data_input, data_output = load_data()</code></pre>



<p>Secondly, split the data using the <code>split_data(data_input, data_output)</code>:</p>



<pre class="wp-block-code"><code class="">X_train, ytrain = split_data(data_input, data_output)</code></pre>



<p>❗ <code><strong>Here, there is no need to use the test set. Indeed, the evaluation of the final model has already been done in <em>Step 9 - Dimensionality reduction: post processing of the model</em> of the <a href="https://github.com/ovh/ai-training-examples/blob/main/notebooks/natural-language-processing/text-classification/miniconda/spam-classifier/notebook-spam-classifier.ipynb" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">notebook</a>.</strong></code></p>



<p>Thirdly, <strong>transform</strong> and <strong>fit</strong> training set. In order to<strong> </strong>prepare<strong> </strong>the data, you can use <code>CountVectorizer</code> from Scikit-Learn to remove <strong>stop-words</strong> and then <code>fit_transform</code> to fit the inputs.</p>



<pre class="wp-block-code"><code class="">vectorizer = CountVectorizer(stop_words='english', binary=True, min_df=10)
Xtrain = vectorizer.fit_transform(X_train.tolist())
Xtrain = Xtrain.toarray()</code></pre>



<p>Fourthly, use the model and index for prediction by calling <code>spam_classifier_model</code> function.</p>



<pre class="wp-block-code"><code class="">model_logistic_regression, index = spam_classifier_model(Xtrain, ytrain)</code></pre>



<p>Find out the full Python code <a href="https://github.com/ovh/ai-training-examples/blob/main/apps/fastapi/spam-classifier-api/model.py" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">here</a>.</p>



<p>Have you successfully defined your model? Good job 🥳 !</p>



<p>Let&#8217;s go for the creation of the API!</p>



<h2 class="wp-block-heading">Build the FastAPI app with Python</h2>



<p>❗ <code><strong>All the codes below are available in the <em>app.py</em> file. You can find the complete Python code of the <em>app.py</em> file <a href="https://github.com/ovh/ai-training-examples/blob/main/apps/fastapi/spam-classifier-api/app.py" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">here</a>.</strong></code></p>



<p>To begin, you can import dependencies for FastAPI app.</p>



<ul class="wp-block-list">
<li>uvicorn</li>



<li>fastapi</li>



<li>pydantic</li>
</ul>



<pre class="wp-block-code"><code class="">import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel
from model import model_logistic_regression, index, vectorizer</code></pre>



<p>In the first place, you can initialize an instance of FastAPI.</p>



<pre class="wp-block-code"><code class="">app = FastAPI()</code></pre>



<p>Next, you can define the data format by creating the Python class named <code>request_body</code>. Here, the <strong>string</strong> (<code>str</code>) format is required.</p>



<pre class="wp-block-code"><code class="">class request_body(BaseModel):
    message : str</code></pre>



<p>Now, you can create the process function in order to prepare the sent message to be used by the model.</p>



<pre class="wp-block-code"><code class="">def process_message(message):

    desc = vectorizer.transform(message)
    dense_desc = desc.toarray()
    dense_select = dense_desc[:, index[0]]

    return dense_select</code></pre>



<p>At the exit of this function the message does not contain any more <strong>stop words</strong>, it is put in the right format for the model thanks to the <code>transform</code> and is then represented as an <code>array</code>.</p>



<p>Now that the function for processing the input data is defined, you can pass the <code>GET</code> and <code>POST</code> methods.</p>



<p>First, let&#8217;s go for the <code>GET</code> method!</p>



<pre class="wp-block-code"><code class="">@app.get('/')
def root():
    return {'message': 'Welcome to the SPAM classifier API'}</code></pre>



<p>Here you can see the <em>welcome message</em> when you arrive on your API.</p>



<pre class="wp-block-preformatted"><code><strong>{"message":"Welcome to the SPAM classifier API"}</strong></code></pre>



<p>Now it&#8217;s the turn of the <code>POST</code> method. In this part of the code, you will be able to:</p>



<ul class="wp-block-list">
<li>define the message format</li>



<li>check if a message has been sent or not</li>



<li>process the message to fit with the model</li>



<li>extract the probabilities</li>



<li>return the results</li>
</ul>



<pre class="wp-block-code"><code class="">@app.post('/spam_detection_path')
def classify_message(data : request_body):

    message = [
        data.message
    ]

    if (not (message)):
        raise HTTPException(status_code=400, detail="Please Provide a valid text message")

    dense_select = process_message(message)

    label = model_logistic_regression.predict(dense_select)
    proba = model_logistic_regression.predict_proba(dense_select)

    if label[0]=='ham':
        label_proba = proba[0][0]
    else:
        label_proba = proba[0][1]

    return {'label': label[0], 'label_probability': label_proba}</code></pre>



<p><code><strong>❗ Again, you can find the full code <a href="https://github.com/ovh/ai-training-examples/blob/main/apps/fastapi/spam-classifier-api/app.py" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">here</a></strong></code>.</p>



<p>Before deploying your API, you can test it locally using the following command:</p>



<pre class="wp-block-code"><code class="">uvicorn app:app --reload</code></pre>



<p>Then, you can test your app locally at the following address:&nbsp;<strong><code>http://localhost:8000/</code></strong></p>



<p>You will arrive on the following page:</p>



<figure class="wp-block-image aligncenter size-full is-resized"><img loading="lazy" decoding="async" src="https://blog.ovhcloud.com/wp-content/uploads/2022/12/image-4.png" alt="" class="wp-image-24217" width="590" height="721" srcset="https://blog.ovhcloud.com/wp-content/uploads/2022/12/image-4.png 760w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/image-4-245x300.png 245w" sizes="auto, (max-width: 590px) 100vw, 590px" /></figure>



<p><strong>How to interact with your&nbsp;API?</strong></p>



<p>You can add&nbsp;<code>/docs</code>&nbsp;at the end of the url of your&nbsp;app: <strong><code>http://localhost:8000/</code></strong><code><strong>docs</strong></code></p>



<p>A new page opens to you. It provides a complete dashboard for interacting with the&nbsp;API!</p>



<figure class="wp-block-image aligncenter size-full is-resized"><img loading="lazy" decoding="async" src="https://blog.ovhcloud.com/wp-content/uploads/2022/12/image.png" alt="" class="wp-image-24213" width="590" height="722" srcset="https://blog.ovhcloud.com/wp-content/uploads/2022/12/image.png 760w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/image-245x300.png 245w" sizes="auto, (max-width: 590px) 100vw, 590px" /></figure>



<p>To be able to send a message for classification, select&nbsp;<code><strong>/spam_detection_path</strong></code>&nbsp;in the green box. Click on<strong>&nbsp;<code>Try</code></strong><code><strong> it out</strong></code>&nbsp;and type the message of your choice in the dedicated&nbsp;zone.</p>



<figure class="wp-block-image aligncenter size-full is-resized"><img loading="lazy" decoding="async" src="https://blog.ovhcloud.com/wp-content/uploads/2022/12/image-2.png" alt="" class="wp-image-24215" width="596" height="729" srcset="https://blog.ovhcloud.com/wp-content/uploads/2022/12/image-2.png 760w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/image-2-245x300.png 245w" sizes="auto, (max-width: 596px) 100vw, 596px" /></figure>



<p>Enter the message of your choice. It must be in the form of a <code><strong>string</strong></code>. </p>



<p><em>Example:</em> <code><strong>"A new free service for you only"</strong></code></p>



<figure class="wp-block-image aligncenter size-full is-resized"><img loading="lazy" decoding="async" src="https://blog.ovhcloud.com/wp-content/uploads/2022/12/image-1.png" alt="" class="wp-image-24214" width="599" height="733" srcset="https://blog.ovhcloud.com/wp-content/uploads/2022/12/image-1.png 760w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/image-1-245x300.png 245w" sizes="auto, (max-width: 599px) 100vw, 599px" /></figure>



<p>To get the result of the prediction, click on the&nbsp;<code><strong>Execute</strong></code>&nbsp;button.</p>



<figure class="wp-block-image aligncenter size-full is-resized"><img loading="lazy" decoding="async" src="https://blog.ovhcloud.com/wp-content/uploads/2022/12/image-3.png" alt="" class="wp-image-24216" width="611" height="748" srcset="https://blog.ovhcloud.com/wp-content/uploads/2022/12/image-3.png 760w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/image-3-245x300.png 245w" sizes="auto, (max-width: 611px) 100vw, 611px" /></figure>



<p>Finally, you obtain the result of the prediction with the&nbsp;<strong>label</strong>&nbsp;and the&nbsp;<strong>confidence&nbsp;score</strong>.</p>



<p>Your app works locally? Congratulations&nbsp;🎉 !</p>



<p>Now it’s time to move on to containerization!</p>



<h2 class="wp-block-heading">Containerize your app with Docker</h2>



<p>First of all, you have to build the file that will contain the different Python modules to be installed with their corresponding version.</p>



<figure class="wp-block-image aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="574" src="https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-docker-1024x574.jpg" alt="docker image datascience" class="wp-image-24230" srcset="https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-docker-1024x574.jpg 1024w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-docker-300x168.jpg 300w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-docker-768x430.jpg 768w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-docker-1536x861.jpg 1536w, https://blog.ovhcloud.com/wp-content/uploads/2022/12/draw-docker.jpg 1620w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h3 class="wp-block-heading">Create the requirements.txt file</h3>



<p>The&nbsp;<code><a href="https://github.com/ovh/ai-training-examples/blob/main/apps/fastapi/spam-classifier-api/requirements.txt" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">requirements.txt</a></code>&nbsp;file will allow us to write all the modules needed to make our application work.</p>



<pre class="wp-block-code"><code class="">fastapi==0.87.0
pydantic==1.10.2
uvicorn==0.20.0
pandas==1.5.1
scikit-learn==1.1.3</code></pre>



<p>This file will be useful when writing the&nbsp;<code>Dockerfile</code>.</p>



<h3 class="wp-block-heading">Write the Dockerfile</h3>



<p>Your&nbsp;<code><a href="https://github.com/ovh/ai-training-examples/blob/main/apps/fastapi/spam-classifier-api/Dockerfile" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Dockerfile</a></code>&nbsp;should start with the the&nbsp;<code>FROM</code>&nbsp;instruction indicating the parent image to use. In our case we choose to start from a classic Python image.</p>



<p>For this Streamlit app, you can use version&nbsp;<strong><code>3.8</code></strong>&nbsp;of Python.</p>



<pre class="wp-block-code"><code class="">FROM python:3.8</code></pre>



<p>Next, you have to to fill in the working directory and add all&nbsp;files into.</p>



<p><code><strong>❗&nbsp;Here you must be in the /workspace directory. This is the basic directory for launching an OVHcloud AI Deploy.</strong></code></p>



<pre class="wp-block-code"><code class="">WORKDIR /workspace
ADD . /workspace</code></pre>



<p>Install the&nbsp;<code>requirements.txt</code>&nbsp;file which contains your needed Python modules using a&nbsp;<code>pip install…</code>&nbsp;command.</p>



<pre class="wp-block-code"><code class="">RUN pip install -r requirements.txt</code></pre>



<p>Set the listening port of the&nbsp;container. For <strong>FastAPI</strong>, you can use the port <code>8000</code>.</p>



<pre class="wp-block-code"><code class="">EXPOSE 8000</code></pre>



<p>Then, you have to define the <strong>entrypoint</strong> and the <strong>default launching command</strong> to start the application.</p>



<pre class="wp-block-code"><code class="">ENTRYPOINT ["uvicorn"]
CMD [ "streamlit", "run", "/workspace/app.py", "--server.address=0.0.0.0" ]</code></pre>



<p>Finally, you can give correct access rights to OVHcloud user (<code>42420:42420</code>).</p>



<pre class="wp-block-code"><code class="">RUN chown -R 42420:42420 /workspace
ENV HOME=/workspace</code></pre>



<p>Once your&nbsp;<code>Dockerfile</code>&nbsp;is defined, you will be able to build your custom docker image.</p>



<h3 class="wp-block-heading">Build the Docker image from the Dockerfile</h3>



<p>First, you can launch the following command from the&nbsp;<code>Dockerfile</code>&nbsp;directory to build your application image.</p>



<pre class="wp-block-code"><code class="">docker build . -t fastapi-spam-classification:latest</code></pre>



<p>⚠️&nbsp;<strong><code>The dot . argument indicates that your build context (place of the Dockerfile and other needed files) is the current directory.</code></strong></p>



<p>⚠️&nbsp;<code><strong>The -t argument allows you to choose the identifier to give to your image. Usually image identifiers are composed of a name and a version tag &lt;name&gt;:&lt;version&gt;. For this example we chose fastapi-spam-classification:latest.</strong></code></p>



<h3 class="wp-block-heading">Test it locally</h3>



<p>Now, you can run the following&nbsp;<strong>Docker command</strong>&nbsp;to launch your application locally on your computer.</p>



<pre class="wp-block-code"><code class="">docker run --rm -it -p 8080:8080 --user=42420:42420 fastapi-spam-classification<span style="background-color: inherit;font-family: inherit;font-size: 1rem;font-weight: inherit">:latest</span></code></pre>



<p>⚠️&nbsp;<code><strong>The -p 8000:8000 argument indicates that you want to execute a port redirection from the port 8000 of your local machine into the port 8000 of the Docker container.</strong></code></p>



<p>⚠️<code><strong>&nbsp;Don't forget the --user=42420:42420 argument if you want to simulate the exact same behaviour that will occur on AI Deploy. It executes the Docker container as the specific OVHcloud user (user 42420:42420).</strong></code></p>



<p>Once started, your application should be available on&nbsp;<strong>http://localhost:8000</strong>.<br><br>Your Docker image seems to work? Good job&nbsp;👍 !<br><br>It’s time to push it and deploy it!</p>



<h3 class="wp-block-heading">Push the image into the shared registry</h3>



<p>❗&nbsp;The shared registry of AI Deploy should only be used for testing purpose. Please consider attaching your own Docker registry. More information about this can be found&nbsp;<a href="https://docs.ovh.com/asia/en/publiccloud/ai/training/add-private-registry/" data-wpel-link="exclude">here</a>.</p>



<p>Then, you have to find the address of your&nbsp;<code>shared registry</code>&nbsp;by launching this command.</p>



<pre class="wp-block-code"><code class="">ovhai registry list</code></pre>



<p>Next, log in on the shared registry with your usual&nbsp;<code>OpenStack</code>&nbsp;credentials.</p>



<pre class="wp-block-code"><code class="">docker login -u &lt;user&gt; -p &lt;password&gt; &lt;shared-registry-address&gt;</code></pre>



<p>To finish, you need to push the created image into the shared registry.</p>



<pre class="wp-block-code"><code class="">docker tag fastapi-spam-classification:latest &lt;shared-registry-address&gt;/fastapi-spam-classification:latest</code></pre>



<pre class="wp-block-code"><code class="">docker push &lt;shared-registry-address&gt;/fastapi-spam-classification:latest</code></pre>



<p>Once you have pushed your custom Docker image into the shared registry, you are ready to launch your app 🚀 !</p>



<h2 class="wp-block-heading">Launch the AI Deploy app</h2>



<p>The following command starts a new job running your <strong>FastAPI</strong> application.</p>



<pre class="wp-block-code"><code class="">ovhai app run \
      --default-http-port 8000 \
      --cpu 4 \
      &lt;shared-registry-address&gt;/fastapi-spam-classification:latest</code></pre>



<h3 class="wp-block-heading">Choose the compute resources</h3>



<p>First, you can either choose the number of GPUs or CPUs for your app.</p>



<p><code><strong>--cpu 4</strong></code>&nbsp;indicates that we request 4 CPUs for that app.</p>



<h3 class="wp-block-heading">Make the app public</h3>



<p>Finally, if you want your app to be accessible without the need to authenticate, specify it as follows.</p>



<p>Consider adding the&nbsp;<code><strong>--unsecure-http</strong></code>&nbsp;attribute if you want your application to be reachable without any authentication.</p>



<figure class="wp-block-video"></figure>



<h2 class="wp-block-heading">Conclusion</h2>



<p>Well done 🎉&nbsp;! You have learned how to build your&nbsp;<strong>own Docker image</strong>&nbsp;for a dedicated&nbsp;<strong>spam classification API</strong>!</p>



<p>You have also been able to deploy this app thanks to&nbsp;<strong>OVHcloud’s AI Deploy</strong>&nbsp;tool.</p>



<h3 class="wp-block-heading" id="want-to-find-out-more">Want to find out more?</h3>



<h5 class="wp-block-heading"><strong>Notebook</strong></h5>



<p>You want to access the notebook? Refer to the&nbsp;<a href="https://github.com/ovh/ai-training-examples/blob/main/notebooks/natural-language-processing/text-classification/miniconda/spam-classifier/notebook-spam-classifier.ipynb" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">GitHub repository</a>.</p>



<h5 class="wp-block-heading"><strong>App</strong></h5>



<p>You want to access to the full code to create the <strong>FastAPI</strong> API? Refer to the&nbsp;<a href="https://github.com/ovh/ai-training-examples/tree/main/apps/fastapi/spam-classifier-api" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">GitHub repository</a>.<br><br>To launch and test this app with&nbsp;<strong>AI Deploy</strong>, please refer to&nbsp;our&nbsp;<a href="https://docs.ovh.com/gb/en/publiccloud/ai/deploy/tuto-fastapi-spam-classifier/" data-wpel-link="exclude">documentation</a>.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://towardsdatascience.com/how-to-run-a-data-science-project-in-a-docker-container-2ab1a3baa889" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">How to Run a Data Science Project in a Docker Container</a></li>



<li><a href="https://towardsdatascience.com/step-by-step-approach-to-build-your-machine-learning-api-using-fast-api-21bd32f2bbdb" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Step-by-step Approach to Build Your Machine Learning API Using Fast API</a></li>
</ul>
<img loading="lazy" decoding="async" src="//blog.ovhcloud.com/wp-content/plugins/matomo/app/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fblog.ovhcloud.com%2Fdeploy-a-custom-docker-image-for-data-science-project-a-spam-classifier-with-fastapi-part-3%2F&amp;action_name=Deploy%20a%20custom%20Docker%20image%20for%20Data%20Science%20project%20%E2%80%93%20A%20spam%20classifier%20with%20FastAPI%20%28Part%203%29&amp;urlref=https%3A%2F%2Fblog.ovhcloud.com%2Ffeed%2F" style="border:0;width:0;height:0" width="0" height="0" alt="" />]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Deploy a custom Docker image for Data Science project – Streamlit app for EDA and interactive prediction (Part 2)</title>
		<link>https://blog.ovhcloud.com/deploy-a-custom-docker-image-for-data-science-project-streamlit-app-for-eda-and-interactive-prediction-part-2/</link>
		
		<dc:creator><![CDATA[Eléa Petton]]></dc:creator>
		<pubDate>Tue, 11 Oct 2022 07:38:35 +0000</pubDate>
				<category><![CDATA[OVHcloud Engineering]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[AI Deploy]]></category>
		<category><![CDATA[AI Solutions]]></category>
		<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[EDA]]></category>
		<category><![CDATA[Machine learning]]></category>
		<category><![CDATA[Open Source]]></category>
		<category><![CDATA[OVHcloud]]></category>
		<category><![CDATA[Public Cloud]]></category>
		<category><![CDATA[python]]></category>
		<category><![CDATA[PyTorch]]></category>
		<category><![CDATA[Streamlit]]></category>
		<guid isPermaLink="false">https://blog.ovhcloud.com/?p=23479</guid>

					<description><![CDATA[A guide to deploy a custom Docker image for a Streamlit app with AI Deploy. Welcome to the second article concerning custom Docker image deployment. If you haven&#8217;t read the previous one, you can read it on the following link. It was about Gradio and sketch recognition. When creating code for a Data Science project, [&#8230;]<img src="//blog.ovhcloud.com/wp-content/plugins/matomo/app/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fblog.ovhcloud.com%2Fdeploy-a-custom-docker-image-for-data-science-project-streamlit-app-for-eda-and-interactive-prediction-part-2%2F&amp;action_name=Deploy%20a%20custom%20Docker%20image%20for%20Data%20Science%20project%20%E2%80%93%20Streamlit%20app%20for%20EDA%20and%20interactive%20prediction%20%28Part%202%29&amp;urlref=https%3A%2F%2Fblog.ovhcloud.com%2Ffeed%2F" style="border:0;width:0;height:0" width="0" height="0" alt="" />]]></description>
										<content:encoded><![CDATA[
<p><em>A guide to deploy a custom Docker image for a <a href="https://streamlit.io/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Streamlit</a> app with <strong>AI Deploy</strong>.</em></p>



<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="817" data-id="23517" src="https://blog.ovhcloud.com/wp-content/uploads/2022/10/image3-1024x817.jpeg" alt="streamlit app for eda and interactive prediction" class="wp-image-23517" srcset="https://blog.ovhcloud.com/wp-content/uploads/2022/10/image3-1024x817.jpeg 1024w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image3-300x239.jpeg 300w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image3-768x613.jpeg 768w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image3-1536x1225.jpeg 1536w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image3.jpeg 1620w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</figure>



<p><em>Welcome to the second article concerning <strong>custom Docker image deployment</strong>. If you haven&#8217;t read the previous one, you can read it on the following <a href="https://blog.ovhcloud.com/deploy-a-custom-docker-image-for-data-science-project-gradio-sketch-recognition-app-part-1/" data-wpel-link="internal">link</a>. It was about <a href="https://gradio.app/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Gradio</a> and sketch recognition.</em></p>



<p>When creating code for a <strong>Data Science project</strong>, you probably want it to be as portable as possible. In other words, it can be run as many times as you like, even on different machines.</p>



<p>Unfortunately, it is often the case that a Data Science code works fine locally on a machine but gives errors during runtime. It can be due to different versions of libraries installed on the host machine.</p>



<p>To deal with this problem, you can use <a href="https://www.docker.com/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Docker</a>.</p>



<p><strong>The article is organized as follows:</strong></p>



<ul class="wp-block-list">
<li>Objectives</li>



<li>Concepts</li>



<li>Load the trained PyTorch model </li>



<li>Build the Streamlit app with Python</li>



<li>Containerize your app with Docker</li>



<li>Launch the app with AI Deploy</li>
</ul>



<p><em>All the code for this blogpost is available in our dedicated <a href="https://github.com/ovh/ai-training-examples/tree/main/apps/streamlit/eda-classification-iris" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">GitHub repository</a>. You can test it with OVHcloud <strong>AI Deploy</strong> tool, please refer to the <a href="https://docs.ovh.com/gb/en/publiccloud/ai/deploy/tuto-streamlit-eda-iris/" data-wpel-link="exclude">documentation</a> to boot it up.</em></p>



<h2 class="wp-block-heading">Objectives</h2>



<p>In this article, you will learn how to develop Streamlit app for two Data Science tasks: Exploratory Data&nbsp;Analysis (<strong>EDA</strong>) and prediction based on ML model.</p>



<p>Once your app is up and running locally, it will be a matter of containerizing it, then deploying the custom Docker image with AI Deploy.</p>



<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-2 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="466" data-id="23521" src="https://blog.ovhcloud.com/wp-content/uploads/2022/10/image4-1024x466.jpeg" alt="objective of streamlit app deployment" class="wp-image-23521" srcset="https://blog.ovhcloud.com/wp-content/uploads/2022/10/image4-1024x466.jpeg 1024w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image4-300x137.jpeg 300w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image4-768x350.jpeg 768w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image4-1536x700.jpeg 1536w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image4.jpeg 1620w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</figure>



<h2 class="wp-block-heading">Concepts</h2>



<p>In Artificial Intelligence, you probably hear about the famous use case of the <a href="https://archive.ics.uci.edu/ml/datasets/iris" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Iris dataset</a>. <strong>How about learning more about the iris dataset?</strong></p>



<h3 class="wp-block-heading">Iris dataset</h3>



<p><strong>Iris Flower Dataset</strong> is considered as the <em>Hello World</em> for Data Science. The <a href="https://en.wikipedia.org/wiki/Iris_flower_data_set" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Iris Flower Dataset</a> contains <strong>four features</strong> (length and width of sepals and petals) of <strong>50 samples</strong> of <strong>three species</strong> of Iris:</p>



<ul class="wp-block-list">
<li>Iris setosa</li>



<li>Iris virginica</li>



<li>Iris versicolor</li>
</ul>



<p>The dataset is in <code>csv</code> format and you can also find it directly as a <code>dataframe</code>. It contains five columns namely: </p>



<ul class="wp-block-list">
<li>Petal length</li>



<li>Petal width</li>



<li>Sepal length</li>



<li>Sepal width</li>



<li>Species type</li>
</ul>



<p>The objective of the models based on this dataset is to classify the three <strong>Iris species</strong>. The measurements of petals and sepals are used to create, for example, a <strong>linear discriminant model</strong> to classify species.</p>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" src="https://blog.ovhcloud.com/wp-content/uploads/2022/10/image0-1024x864.jpeg" alt="iris dataset" class="wp-image-23522" width="646" height="545" srcset="https://blog.ovhcloud.com/wp-content/uploads/2022/10/image0-1024x864.jpeg 1024w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image0-300x253.jpeg 300w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image0-768x648.jpeg 768w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image0-1536x1297.jpeg 1536w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image0.jpeg 1592w" sizes="auto, (max-width: 646px) 100vw, 646px" /></figure>



<p>❗ <code><strong>A model to classify Iris species was trained in a previous tutorial, in notebook form, which you can find and test<a href="https://github.com/ovh/ai-training-examples/blob/main/notebooks/computer-vision/image-classification/tensorflow/weights-and-biases/notebook_Weights_and_Biases_MNIST.ipynb" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"> </a><a href="https://github.com/ovh/ai-training-examples/blob/main/notebooks/getting-started/pytorch/notebook_classification_iris.ipynb" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">here</a>.</strong></code></p>



<p>This model is registered in an OVHcloud&nbsp;<a href="https://docs.ovh.com/gb/en/publiccloud/ai/cli/data-cli/" data-wpel-link="exclude">Object Storage container</a>.</p>



<p>In this article, the first objective is to create an app for Exploratory Data&nbsp;Analysis (<strong>EDA</strong>). Then you will see how to obtain interactive prediction.</p>



<h3 class="wp-block-heading">EDA</h3>



<p><strong>What is EDA in Data Science?</strong></p>



<p><a href="https://en.wikipedia.org/wiki/Exploratory_data_analysis" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Exploratory Data Analysis</a> (<strong>EDA</strong>) is a technique to analyze data with visual techniques. In this way, you have detailed information about the statistical summary of the data. </p>



<p>In addition, <strong>EDA</strong> allows duplicate values, outliers to be dealt with, and also to see certain trends or patterns present in the dataset.</p>



<p>For Iris dataset, the aim is to observe the source data on visual graphs using the <strong>Streamlit</strong> tool.</p>



<h3 class="wp-block-heading">Streamlit</h3>



<p><strong>What is Streamlit?</strong></p>



<p><a href="https://streamlit.io/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Streamlit</a>&nbsp;allows you to transform data scripts into quickly shareable web applications using only the <strong>Python</strong> language. Moreover, this framework does not require front-end skills.</p>



<p>This is a time saver for the data scientist who wants to deploy an app around the world of data!</p>



<p>To make this app accessible, you need to containerize it using&nbsp;<strong>Docker</strong>.</p>



<h3 class="wp-block-heading">Docker</h3>



<p><a href="https://www.docker.com/" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Docker</a>&nbsp;platform allows you to build, run and manage isolated applications. The principle is to build an application that contains not only the written code but also all the context to run the code: libraries and their versions for example</p>



<p>When you wrap your application with all its context, you build a Docker image, which can be saved in your local repository or in the Docker Hub.</p>



<p>To get started with Docker, please, check this&nbsp;<a href="https://www.docker.com/get-started" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">documentation</a>.</p>



<p>To build a Docker image, you will define 2 elements:</p>



<ul class="wp-block-list">
<li>the application code (<em>Streamlit app</em>)</li>



<li>the&nbsp;<a href="https://docs.docker.com/engine/reference/builder/" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Dockerfile</a></li>
</ul>



<p>In the next steps, you will see how to develop the Python code for your app, but also how to write the Dockerfile.</p>



<p>Finally, you will see how to deploy your custom docker image with&nbsp;<strong>OVHcloud AI Deploy</strong>&nbsp;tool.</p>



<h3 class="wp-block-heading">AI Deploy</h3>



<p><strong>AI Deploy</strong>&nbsp;enables AI models and managed applications to be started via Docker containers.</p>



<p>To know more about AI Deploy, please refer to this&nbsp;<a href="https://docs.ovh.com/gb/en/publiccloud/ai/deploy/getting-started/" data-wpel-link="exclude">documentation</a>.</p>



<h2 class="wp-block-heading">Load the trained PyTorch model </h2>



<p>❗ <strong><code>To develop an app that uses a machine learning model, you must first load the model in the correct format. For this tutorial, a PyTorch model is used and the Python file utils.py is used to load it</code></strong>.</p>



<p>The first step is to import the&nbsp;<strong>Python libraries</strong>&nbsp;needed to load a PyTorch model in the <code>utils.py</code> file.</p>



<pre class="wp-block-code"><code class="">import torch
import torch.nn as nn
import torch.nn.functional as F</code></pre>



<p>To load your <strong>PyTorch model</strong>, it is first necessary to define its model architecture by using the <code>Model</code> class defined previously in the part &#8220;<em>Step 2 &#8211; Define the neural network model</em>&#8221; of the <a href="https://github.com/ovh/ai-training-examples/blob/main/notebooks/getting-started/pytorch/notebook_classification_iris.ipynb" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">notebook</a>.</p>



<pre class="wp-block-code"><code class="">class Model(nn.Module):
    def __init__(self):

        super().__init__()
        self.layer1 = nn.Linear(in_features=4, out_features=16)
        self.layer2 = nn.Linear(in_features=16, out_features=12)
        self.output = nn.Linear(in_features=12, out_features=3)

    def forward(self, x):

        x = F.relu(self.layer1(x))
        x = F.relu(self.layer2(x))
        x = self.output(x)

        return x</code></pre>



<p>In a second step, you fill in the access path to the model. To save this model in <code>pth</code> format, refer to the part &#8220;<em>Step 6 &#8211; Save the model for future inference</em>&#8221; of the <a href="https://github.com/ovh/ai-training-examples/blob/main/notebooks/getting-started/pytorch/notebook_classification_iris.ipynb" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">notebook</a>.</p>



<pre class="wp-block-code"><code class="">path = "model_iris_classification.pth"</code></pre>



<p>Then, the <code>load_checkpoint</code> function is used to load the model&#8217;s checkpoint.</p>



<pre class="wp-block-code"><code class="">def load_checkpoint(path):

    model = Model()
    print("Model display: ", model)
    model.load_state_dict(torch.load(path))
    model.eval()

    return model</code></pre>



<p>Finally, the function <code>load_model</code> is used to load the model and to use it to obtain the result of the prediction.</p>



<pre class="wp-block-code"><code class="">def load_model(X_tensor):

    model = load_checkpoint(path)
    predict_out = model(X_tensor)
    _, predict_y = torch.max(predict_out, 1)

    return predict_out.squeeze().detach().numpy(), predict_y.item()</code></pre>



<p>Find out the full Python code <a href="https://github.com/ovh/ai-training-examples/blob/main/apps/streamlit/eda-classification-iris/utils.py" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">here</a>.</p>



<p>Have you successfully loaded your model? Good job 🥳 !</p>



<p>Let&#8217;s go for the creation of the Streamlit app!</p>



<h2 class="wp-block-heading">Build the Streamlit app with Python </h2>



<p>❗ <code><strong>All the codes below are available in the <em>app.py</em> file. The key functions are explained in this article.<br>However, the "<em>main</em>" part of the <em>app.py</em> file is not described. You can find the complete Python code of the <em>app.py</em> file <a href="https://github.com/ovh/ai-training-examples/blob/main/apps/streamlit/eda-classification-iris/app.py" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">here</a>.</strong></code></p>



<p>To begin, you can import dependencies for Streamlit app.</p>



<ul class="wp-block-list">
<li>Numpy</li>



<li>Pandas</li>



<li>Seaborn</li>



<li><code>load_model</code> function from utils.py</li>



<li>Torch</li>



<li>Streamlit</li>



<li>Scikit-Learn</li>



<li>Ploty</li>



<li>PIL</li>
</ul>



<pre class="wp-block-code"><code class="">import numpy as np
import pandas as pd
import seaborn as sns
from utils import load_model
import torch
import streamlit as st
from sklearn.datasets import load_iris
from sklearn.decomposition import PCA
import plotly.graph_objects as go
import plotly.express as px
from PIL import Image</code></pre>



<p>Then, you must load source dataset of <strong>Iris flowers</strong> to be able to extract the characteristics and thus, visualize data. Scikit-Learn allows to load this dataset without having to download it!</p>



<p>Next, you can separate the dataset in an <strong>input dataframe</strong> and an <strong>output dataframe</strong>.</p>



<p>Finally, this <code>load_data</code> function is cached so that you don&#8217;t have to download again the dataset.</p>



<pre class="wp-block-code"><code class="">@st.cache
def load_data():
    dataset_iris = load_iris()
    df_inputs = pd.DataFrame(dataset_iris.data, columns=dataset_iris.feature_names)
    df_output = pd.DataFrame(dataset_iris.target, columns=['variety'])

    return df_inputs, df_output</code></pre>



<p>The creation of this Streamlit app is separated into two parts.</p>



<p>Firstly, you can look into the creation of the EDA part. Then you will see how to create an interactive prediction tool using the PyTorch model.</p>



<h3 class="wp-block-heading">EDA on Iris Dataset</h3>



<p>As a first step, you can look at the source dataset by displaying different graphs using the Python <strong>Seaborn</strong> library.</p>



<p><strong>Seaborn Pairplot</strong> allows you to get the relationship between each variable present in <strong>Pandas</strong> dataframe. </p>



<p><code>sns.pairplot</code> plots the graph in pairs of several features in a grid format.</p>



<pre class="wp-block-code"><code class="">@st.cache(allow_output_mutation=True)
def data_visualization(df_inputs, df_output):

    df = pd.concat([df_inputs, df_output['variety']], axis=1)
    eda = sns.pairplot(data=df, hue="variety", palette=['#0D0888', '#CB4779', '#F0F922'])

    return eda</code></pre>



<p>Later, this function will display the following graph thanks to a call in the &#8220;<code><em>main</em></code>&#8221; of <code><a href="https://github.com/ovh/ai-training-examples/blob/main/apps/streamlit/eda-classification-iris/app.py" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">app.py</a></code> file.</p>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" src="https://blog.ovhcloud.com/wp-content/uploads/2022/10/image-1024x956.png" alt="iris data visualization / eda with sns.pairplot" class="wp-image-23487" width="756" height="706" srcset="https://blog.ovhcloud.com/wp-content/uploads/2022/10/image-1024x956.png 1024w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image-300x280.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image-768x717.png 768w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image.png 1460w" sizes="auto, (max-width: 756px) 100vw, 756px" /></figure>



<p>Here it can be seen that the&nbsp;<code><strong>setosa</strong> 0</code>&nbsp;variety is easily separated from the other two (<code><strong>versicolor</strong>&nbsp;1</code>&nbsp;and&nbsp;<code><strong>virginica</strong>&nbsp;2</code>).</p>



<p>Were you able to display your graph? Well done 🎉 !</p>



<p>So, let&#8217;s go to the <strong>interactive prediction</strong> tool 🔜 !</p>



<h3 class="wp-block-heading">Create an interactive prediction tool</h3>



<p>To create an interactive prediction tool, you will need several elements:</p>



<ul class="wp-block-list">
<li>Firstly, you need <strong>four sliders</strong> to play with the input parameters</li>



<li>Secondly, you have to create a function to display the <strong>Principal Component Analysis</strong> (<strong>PCA</strong>) graph to visualize the point corresponding to the output of the model</li>



<li>Thirdly, you can build a <strong>histogram</strong> representing the result of the prediction</li>



<li>Fourthly, you will have a function to <strong>display the image</strong> of the predicted Iris species</li>
</ul>



<p>Ready to go? Let&#8217;s start creating <strong>sliders</strong>!</p>



<h4 class="wp-block-heading">Create a sidebar with sliders for input data</h4>



<p>In order to facilitate the visual reading of the Streamlit app, sliders are added in a <strong>sidebar</strong>.</p>



<p>In this sidebar, four sliders are added so that users can choose the length and width of petals and sepals.</p>



<p><strong>How to create a slider?</strong> Well, nothing could be easier than with Streamlit!</p>



<p>You need to define the function <code>st.sidebar.slider()</code> to <strong>add a slider to the sidebar</strong>. Then you can specify arguments such as <strong>minimum</strong> and <strong>maximum</strong> values or the average value which will be the default value. Finally, you can specify the <strong>step</strong> of your slider.</p>



<p>❗ <code><strong>Here you can see the example for a single slider. Find the complete code of the other sliders on the GitHub repo <a href="https://github.com/ovh/ai-training-examples/blob/main/apps/streamlit/eda-classification-iris/app.py" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">here</a>.</strong></code></p>



<pre class="wp-block-code"><code class="">def create_slider(df_inputs):

    sepal_length = st.sidebar.slider(
        label='Sepal Length',
        min_value=float(df_inputs['sepal length (cm)'].min()),
        max_value=float(df_inputs['sepal length (cm)'].max()),
        value=float(round(df_inputs['sepal length (cm)'].mean(), 1)),
        step=0.1)

    sepal_width = st.sidebar.slider(
        ...
        )

    petal_length = st.sidebar.slider(
        ...
        )

    petal_width = st.sidebar.slider(
        ...
        )

    return sepal_length, sepal_width, petal_length, petal_width</code></pre>



<p>Later, this function will be call in the &#8220;<code><em>main</em></code>&#8221; of the <code><a href="https://github.com/ovh/ai-training-examples/blob/main/apps/streamlit/eda-classification-iris/app.py" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">app.py</a></code> file. Afterwards, you will see the following interface:</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" src="https://blog.ovhcloud.com/wp-content/uploads/2022/10/image-1.png" alt="Streamlit sidebar with sliders" class="wp-image-23493" width="625" height="665"/></figure>



<p>Thanks to these <strong>sliders</strong>, you can now obtain the result of the prediction in an interactive way by playing on <strong>one or more parameters</strong>.</p>



<h4 class="wp-block-heading">Display PCA graph</h4>



<p>Once your sliders are up and running, you can create a function to display the graph of the <strong>Principal Component Analysis</strong> (<strong>PCA</strong>).</p>



<p><strong>PCA</strong> is a technique that transforms <strong>high-dimensional</strong> data into <strong>lower dimensions</strong> while retaining as much information as possible.</p>



<p><strong>What about the Iris dataset?</strong> The aim is to be able to display the point resulting from the model prediction on a<strong> two-dimensional graph</strong>.</p>



<p>The <code>run_pca</code> function below displays the <strong>two-dimensional</strong> graph with iris of the source dataset.</p>



<pre class="wp-block-code"><code class="">@st.cache
def run_pca():

    pca = PCA(2)
    X = df_inputs.iloc[:, :4]
    X_pca = pca.fit_transform(X)
    df_pca = pd.DataFrame(pca.transform(X))
    df_pca.columns = ['PC1', 'PC2']
    df_pca = pd.concat([df_pca, df_output['variety']], axis=1)

    return pca, df_pca</code></pre>



<p>Thereafter, the black point corresponding to the result of the prediction is placed on the same graph in the &#8220;<code><em>main</em></code>&#8221; of the Python <a href="https://github.com/ovh/ai-training-examples/blob/main/apps/streamlit/eda-classification-iris/app.py" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer"><code>app.py</code></a> file.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="700" height="450" src="https://blog.ovhcloud.com/wp-content/uploads/2022/10/newplot-1-1.png" alt="Principal Component Analysis (PCA) Iris dataset" class="wp-image-23498" srcset="https://blog.ovhcloud.com/wp-content/uploads/2022/10/newplot-1-1.png 700w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/newplot-1-1-300x193.png 300w" sizes="auto, (max-width: 700px) 100vw, 700px" /></figure>



<p>With this method you were able to visualize your point in space. However, the numerical result of the prediction is not filled in.</p>



<p>Therefore, you can also display the results as a histogram.</p>



<h4 class="wp-block-heading">Return predictions histogram</h4>



<p>At the output of the neural network, the results can be <strong>positive or negative</strong> and the highest value corresponds to the iris species predicted by the model.</p>



<p>To create a histogram, negative values can be removed. To do this, the predictions with <strong>positive values</strong> are extracted and sent to a list before being transformed into a dataframe.</p>



<p>The negative values are all replaced by the null value.</p>



<p>To summarize, the <code>extract_positive_value</code> function can be translated into the following mathematical formula: <br><code>f(prediction) = max(0, prediction)</code></p>



<pre class="wp-block-code"><code class="">def extract_positive_value(prediction):

    prediction_positive = []
    for p in prediction:
        if p &lt; 0:
            p = 0
        prediction_positive.append(p)

    return pd.DataFrame({'Species': ['Setosa', 'Versicolor', 'Virginica'], 'Confidence': prediction_positive})</code></pre>



<p>This function is then called to build the histogram in the &#8220;<code><em>main</em></code>&#8221; of the Python <a href="https://github.com/ovh/ai-training-examples/blob/main/apps/streamlit/eda-classification-iris/app.py" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer"><code>app.py</code></a> file. The library <code>plotly</code> allows to build this <strong>bar chart</strong> as follows.</p>



<pre class="wp-block-code"><code class="">fig = px.bar(extract_positive_value(prediction), x='Species', y='Confidence', width=400, height=400, color='Species', color_discrete_sequence=['#0D0888', '#CB4779', '#F0F922'])</code></pre>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" src="https://blog.ovhcloud.com/wp-content/uploads/2022/10/newplot-2.png" alt="Histogram prediction iris species" class="wp-image-23499" width="388" height="388" srcset="https://blog.ovhcloud.com/wp-content/uploads/2022/10/newplot-2.png 400w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/newplot-2-300x300.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/newplot-2-150x150.png 150w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/newplot-2-70x70.png 70w" sizes="auto, (max-width: 388px) 100vw, 388px" /></figure>



<h4 class="wp-block-heading">Show Iris species image</h4>



<p>The final step is to display the predicted iris image using a <strong>Streamlit button</strong>. Therefore, you can define the display_image function to select the correct image based on the prediction.</p>



<pre class="wp-block-code"><code class="">def display_img(species):

    list_img = ['setosa.png', 'versicolor.png', 'virginica.png']

    return Image.open(list_img[species])</code></pre>



<p>Finally, in the main Python code <code><a href="https://github.com/ovh/ai-training-examples/blob/main/apps/streamlit/eda-classification-iris/app.py" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">app.py</a></code>, <code>st.image()</code> displays the image when the user requests it by pressing the &#8220;<code>Show flower image</code>&#8221; button.</p>



<pre class="wp-block-code"><code class="">if st.button('Show flower image'):
    st.image(display_img(species), width=300)
    st.write(df_pred.iloc[species, 0])</code></pre>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="347" height="327" src="https://blog.ovhcloud.com/wp-content/uploads/2022/10/image-2.png" alt="Streamlit button and image displayed" class="wp-image-23500" srcset="https://blog.ovhcloud.com/wp-content/uploads/2022/10/image-2.png 347w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image-2-300x283.png 300w" sizes="auto, (max-width: 347px) 100vw, 347px" /></figure>



<p><code><strong>❗ Again, you can find the full code <a href="https://github.com/ovh/ai-training-examples/blob/main/apps/streamlit/eda-classification-iris/app.py" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">here</a></strong></code>.</p>



<p>Before deploying your Streamlit app, you can test it locally using the following command:</p>



<pre class="wp-block-code"><code class="">streamlit run app.py</code></pre>



<p>Then, you can test your app locally at the following address:&nbsp;<strong>http://localhost:8080/</strong></p>



<p>Your app works locally? Congratulations&nbsp;🎉 !</p>



<p>Now it’s time to move on to containerization!</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="883" height="975" src="https://blog.ovhcloud.com/wp-content/uploads/2022/10/image-5.png" alt="overview streamlit app for eda and prediction on iris data" class="wp-image-23508" srcset="https://blog.ovhcloud.com/wp-content/uploads/2022/10/image-5.png 883w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image-5-272x300.png 272w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image-5-768x848.png 768w" sizes="auto, (max-width: 883px) 100vw, 883px" /></figure>



<h2 class="wp-block-heading">Containerize your app with Docker</h2>



<p>First of all, you have to build the file that will contain the different Python modules to be installed with their corresponding version.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="545" src="https://blog.ovhcloud.com/wp-content/uploads/2022/10/image5-1024x545.jpeg" alt="docker image data science" class="wp-image-23518" srcset="https://blog.ovhcloud.com/wp-content/uploads/2022/10/image5-1024x545.jpeg 1024w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image5-300x160.jpeg 300w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image5-768x409.jpeg 768w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image5-1536x818.jpeg 1536w, https://blog.ovhcloud.com/wp-content/uploads/2022/10/image5.jpeg 1591w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h3 class="wp-block-heading">Create the requirements.txt file</h3>



<p>The&nbsp;<code><a href="https://github.com/ovh/ai-training-examples/blob/main/apps/streamlit/eda-classification-iris/requirements.txt" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">requirements.txt</a></code>&nbsp;file will allow us to write all the modules needed to make our application work.</p>



<pre class="wp-block-code"><code class="">pandas==1.4.4
numpy==1.23.2
torch==1.12.1
streamlit==1.12.2
scikit-learn==1.1.2
plotly==5.10.0
Pillow==9.2.0
seaborn==0.12.0</code></pre>



<p>This file will be useful when writing the&nbsp;<code>Dockerfile</code>.</p>



<h3 class="wp-block-heading">Write the Dockerfile</h3>



<p>Your&nbsp;<code><a href="https://github.com/ovh/ai-training-examples/blob/main/apps/streamlit/eda-classification-iris/Dockerfile" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Dockerfile</a></code>&nbsp;should start with the the&nbsp;<code>FROM</code>&nbsp;instruction indicating the parent image to use. In our case we choose to start from a classic Python image.</p>



<p>For this Streamlit app, you can use version&nbsp;<strong>3.8</strong>&nbsp;of Python.</p>



<pre class="wp-block-code"><code class="">FROM python:3.8</code></pre>



<p>Next, you have to to fill in the working directory and add all&nbsp;files into.</p>



<p><code><strong>❗&nbsp;Here you must be in the /workspace directory. This is the basic directory for launching an OVHcloud AI Deploy.</strong></code></p>



<pre class="wp-block-code"><code class="">WORKDIR /workspace
ADD . /workspace</code></pre>



<p>Install the&nbsp;<code>requirements.txt</code>&nbsp;file which contains your needed Python modules using a&nbsp;<code>pip install…</code>&nbsp;command:</p>



<pre class="wp-block-code"><code class="">RUN pip install -r requirements.txt</code></pre>



<p>Then, you can give correct access rights to OVHcloud user (<code>42420:42420</code>).</p>



<pre class="wp-block-code"><code class="">RUN chown -R 42420:42420 /workspace
ENV HOME=/workspace</code></pre>



<p>Finally, you have to define your default launching command to start the application.</p>



<pre class="wp-block-code"><code class="">CMD [ "streamlit", "run", "/workspace/app.py", "--server.address=0.0.0.0" ]</code></pre>



<p>Once your&nbsp;<code>Dockerfile</code>&nbsp;is defined, you will be able to build your custom docker image.</p>



<h3 class="wp-block-heading">Build the Docker image from the Dockerfile</h3>



<p>First, you can launch the following command from the&nbsp;<code>Dockerfile</code>&nbsp;directory to build your application image.</p>



<pre class="wp-block-code"><code class="">docker build . -t streamlit-eda-iris:latest</code></pre>



<p>⚠️&nbsp;<strong><code>The dot . argument indicates that your build context (place of the Dockerfile and other needed files) is the current directory.</code></strong></p>



<p>⚠️&nbsp;<code><strong>The -t argument allows you to choose the identifier to give to your image. Usually image identifiers are composed of a name and a version tag &lt;name&gt;:&lt;version&gt;. For this example we chose streamlit-eda-iris:latest.</strong></code></p>



<h3 class="wp-block-heading">Test it locally</h3>



<p>Now, you can run the following&nbsp;<strong>Docker command</strong>&nbsp;to launch your application locally on your computer.</p>



<pre class="wp-block-code"><code class="">docker run --rm -it -p 8501:8501 --user=42420:42420 <strong style="background-color: inherit;font-family: inherit;font-size: inherit">streamlit-eda-iris</strong><span style="background-color: inherit;font-family: inherit;font-size: 1rem;font-weight: inherit">:latest</span></code></pre>



<p>⚠️&nbsp;<code><strong>The -p 8501:8501 argument indicates that you want to execute a port redirection from the port 8501 of your local machine into the port 8501 of the Docker container.</strong></code></p>



<p>⚠️<code><strong>&nbsp;Don't forget the --user=42420:42420 argument if you want to simulate the exact same behaviour that will occur on AI Deploy. It executes the Docker container as the specific OVHcloud user (user 42420:42420).</strong></code></p>



<p>Once started, your application should be available on&nbsp;<strong>http://localhost:8080</strong>.<br><br>Your Docker image seems to work? Good job&nbsp;👍 !<br><br>It’s time to push it and deploy it!</p>



<h3 class="wp-block-heading">Push the image into the shared registry</h3>



<p>❗&nbsp;The shared registry of AI Deploy should only be used for testing purpose. Please consider attaching your own Docker registry. More information about this can be found&nbsp;<a href="https://docs.ovh.com/asia/en/publiccloud/ai/training/add-private-registry/" data-wpel-link="exclude">here</a>.</p>



<p>Then, you have to find the address of your&nbsp;<code>shared registry</code>&nbsp;by launching this command.</p>



<pre class="wp-block-code"><code class="">ovhai registry list</code></pre>



<p>Next, log in on the shared registry with your usual&nbsp;<code>OpenStack</code>&nbsp;credentials.</p>



<pre class="wp-block-code"><code class="">docker login -u &lt;user&gt; -p &lt;password&gt; &lt;shared-registry-address&gt;</code></pre>



<p>To finish, you need to push the created image into the shared registry.</p>



<pre class="wp-block-code"><code class="">docker tag streamlit-eda-iris:latest &lt;shared-registry-address&gt;/streamlit-eda-iris:latest
docker push &lt;shared-registry-address&gt;/streamlit-eda-iris:latest</code></pre>



<p>Once you have pushed your custom docker image into the shared registry, you are ready to launch your app 🚀 !</p>



<h2 class="wp-block-heading">Launch the AI Deploy app</h2>



<p>The following command starts a new job running your Streamlit application.</p>



<pre class="wp-block-code"><code class="">ovhai app run \
      --default-http-port 8501 \
      --cpu 12 \
      &lt;shared-registry-address&gt;/streamlit-eda-iris:latest</code></pre>



<h3 class="wp-block-heading">Choose the compute resources</h3>



<p>First, you can either choose the number of GPUs or CPUs for your app.</p>



<p><code><strong>--cpu 12</strong></code>&nbsp;indicates that we request 12 CPUs for that app.</p>



<p>If you want, you can also launch this app with one or more&nbsp;<strong>GPUs</strong>.</p>



<h3 class="wp-block-heading">Make the app public</h3>



<p>Finally, if you want your app to be accessible without the need to authenticate, specify it as follows.</p>



<p>Consider adding the&nbsp;<code><strong>--unsecure-http</strong></code>&nbsp;attribute if you want your application to be reachable without any authentication.</p>



<figure class="wp-block-video"><video height="998" style="aspect-ratio: 1917 / 998;" width="1917" controls src="https://blog.ovhcloud.com/wp-content/uploads/2022/10/Enregistrement-de-lécran-2022-10-05-à-11.52.19-1.mov"></video></figure>



<h2 class="wp-block-heading">Conclusion</h2>



<p>Well done 🎉&nbsp;! You have learned how to build your&nbsp;<strong>own Docker image</strong>&nbsp;for a dedicated&nbsp;<strong>EDA and interactive prediction app</strong>!</p>



<p>You have also been able to deploy this app thanks to&nbsp;<strong>OVHcloud’s AI Deploy</strong>&nbsp;tool.</p>



<p><em>In a third article, you will see how it is possible to deploy a Data Science project with an API for&nbsp;Spam classification.</em></p>



<h3 class="wp-block-heading" id="want-to-find-out-more">Want to find out more?</h3>



<h5 class="wp-block-heading"><strong>Notebook</strong></h5>



<p>You want to access the notebook? Refer to the&nbsp;<a href="https://github.com/ovh/ai-training-examples/blob/main/notebooks/getting-started/pytorch/notebook_classification_iris.ipynb" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">GitHub repository</a>.</p>



<h5 class="wp-block-heading"><strong>App</strong></h5>



<p>You want to access to the full code to create the Streamlit app? Refer to the&nbsp;<a href="https://github.com/ovh/ai-training-examples/tree/main/apps/streamlit/eda-classification-iris" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">GitHub repository</a>.<br><br>To launch and test this app with&nbsp;<strong>AI Deploy</strong>, please refer to&nbsp;our&nbsp;<a href="https://docs.ovh.com/gb/en/publiccloud/ai/deploy/tuto-streamlit-eda-iris/" data-wpel-link="exclude">documentation</a>.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://towardsdatascience.com/how-to-run-a-data-science-project-in-a-docker-container-2ab1a3baa889" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">How to Run a Data Science Project in a Docker Container</a></li>



<li><a href="https://medium.com/geekculture/create-a-machine-learning-web-app-with-streamlit-f28c75f9f40f" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Create a Machine Learning Web App with Streamlit</a></li>
</ul>
<img loading="lazy" decoding="async" src="//blog.ovhcloud.com/wp-content/plugins/matomo/app/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fblog.ovhcloud.com%2Fdeploy-a-custom-docker-image-for-data-science-project-streamlit-app-for-eda-and-interactive-prediction-part-2%2F&amp;action_name=Deploy%20a%20custom%20Docker%20image%20for%20Data%20Science%20project%20%E2%80%93%20Streamlit%20app%20for%20EDA%20and%20interactive%20prediction%20%28Part%202%29&amp;urlref=https%3A%2F%2Fblog.ovhcloud.com%2Ffeed%2F" style="border:0;width:0;height:0" width="0" height="0" alt="" />]]></content:encoded>
					
		
		<enclosure url="https://blog.ovhcloud.com/wp-content/uploads/2022/10/Enregistrement-de-lécran-2022-10-05-à-11.52.19-1.mov" length="6370587" type="video/quicktime" />

			</item>
		<item>
		<title>OpenAPI with Python — a state of the art and our latest contribution</title>
		<link>https://blog.ovhcloud.com/openapi-with-python-a-state-of-the-art-and-our-latest-contribution/</link>
		
		<dc:creator><![CDATA[François Magimel]]></dc:creator>
		<pubDate>Fri, 05 Feb 2021 16:20:04 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[APIs]]></category>
		<category><![CDATA[Open Source]]></category>
		<category><![CDATA[OpenAPI]]></category>
		<category><![CDATA[python]]></category>
		<guid isPermaLink="false">https://www.ovh.com/blog/?p=20595</guid>

					<description><![CDATA[At OVHcloud, we love using and building APIs. And to build good software, the first thing you need to do is look at the state of the art in your domain. As a matter of fact, there are more and more tools available and it&#8217;s often hard to make a choice without comparisons. Maybe you&#8217;ll [&#8230;]<img src="//blog.ovhcloud.com/wp-content/plugins/matomo/app/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fblog.ovhcloud.com%2Fopenapi-with-python-a-state-of-the-art-and-our-latest-contribution%2F&amp;action_name=OpenAPI%20with%20Python%20%E2%80%94%20a%20state%20of%20the%20art%20and%20our%20latest%20contribution&amp;urlref=https%3A%2F%2Fblog.ovhcloud.com%2Ffeed%2F" style="border:0;width:0;height:0" width="0" height="0" alt="" />]]></description>
										<content:encoded><![CDATA[
<p>At OVHcloud, we love using and building APIs. And to build good software, the first thing you need to do is look at the state of the art in your domain. As a matter of fact, there are more and more tools available and it&#8217;s often hard to make a choice without comparisons. Maybe you&#8217;ll be tempted to build a new module instead of contributing to an existing one. </p>



<div class="wp-block-image"><figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="537" src="https://www.ovh.com/blog/wp-content/uploads/2021/02/IMG_0473-1024x537.png" alt="OpenAPI with Python — a state of the art and our latest contribution" class="wp-image-20621" srcset="https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0473-1024x537.png 1024w, https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0473-300x157.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0473-768x403.png 768w, https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0473.png 1200w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure></div>



<p>We have just open-sourced a Python module, <a href="https://github.com/ovh/python-apispec-fromfile" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">apispec-fromfile</a>, to simplify the usage of OpenAPI in Python by  importing <a href="https://github.com/OAI/OpenAPI-Specification" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">OpenAPI specifications</a>  from files. And to better explain why we do it, here you have a state of the art about OpenAPI with Python (and more specifically with <a href="https://flask.palletsprojects.com" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Flask</a>).</p>



<h2 class="wp-block-heading" id="Stateoftheart:OpenAPIwithPython-WhatisOpenAPI?">What is OpenAPI?</h2>



<p>As you can read on the official website, the <a href="https://swagger.io/resources/open-api/" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">OpenAPI Specification</a> is &#8220;a broadly adopted industry standard for describing modern APIs”. This standard, formerly named <a href="https://swagger.io/" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Swagger</a>, is used to describe, produce, consume, and&nbsp;visualize APIs in a vendor neutral format. This format is based on JSON Schema and specification files can be either in YAML or JSON format.</p>



<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img loading="lazy" decoding="async" src="https://www.ovh.com/blog/wp-content/uploads/2021/02/IMG_0471-1024x149.png" alt="Swagger &amp; OpenAPI Initiative" class="wp-image-20620" width="512" height="75" srcset="https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0471-1024x149.png 1024w, https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0471-300x44.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0471-768x112.png 768w, https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0471.png 1314w" sizes="auto, (max-width: 512px) 100vw, 512px" /></figure></div>



<p>Nowadays, there are two versions in the wild: 2 and 3. Version 2 is the Swagger specification and is quite common thanks to the many tools available. Version 3 is the latest one, the first one from the <a href="https://www.openapis.org/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">OpenAPI Initiative (OAI)</a>.</p>



<pre title="OpenAPI Specification example" class="wp-block-code"><code lang="yaml" class="language-yaml line-numbers"><code>openapi:</code> <code>3.0.3</code>
<code>info:</code>
  <code>title:</code> <code>My Cutie Marks Catalog</code>
  <code>description:</code> <code>This is a sample server for a cutie marks catalog.</code>
  <code>termsOfService:</code> <code>http://example.com/terms/</code>
  <code>contact:</code>
    <code>name:</code> <code>API Support</code>
    <code>url:</code> <code>http://www.example.com/support</code>
    <code>email:</code> <code>support@example.com</code>
  <code>license:</code>
    <code>name:</code> <code>Apache 2.0</code>
    <code>url:</code> <code>https://www.apache.org/licenses/LICENSE-2.0.html</code>
  <code>version:</code> <code>1.0.1</code></code></pre>



<p><em>Related links:</em></p>



<ul class="wp-block-list"><li><a href="https://en.wikipedia.org/wiki/OpenAPI_Specification" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">OpenAPI Specification (Wikipédia)</a></li><li><a href="https://www.openapis.org" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">OpenAPI Initiative website</a></li><li><a href="https://rapidapi.com/blog/api-glossary/openapi/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">What is OpenAPI?</a></li></ul>



<h2 class="wp-block-heading" id="Stateoftheart:OpenAPIwithPython-Whywouldyouuseit?">Why would you use it?</h2>



<p>Well, there are two main purposes:</p>



<ul class="wp-block-list"><li>to describe your API, with nice documentation (and potentially try your endpoints with it directly)</li><li>to generate your API.</li></ul>



<p>Moreover, you will want to use the latest version of the OpenAPI Specification to take advantage of the new features: the version 3.</p>



<p><em>Related links:</em></p>



<ul class="wp-block-list"><li><a href="https://doi.org/10.1145/3184558.3188740" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">The Exploitation of OpenAPI Documentation for the Generation of Web Frontends</a></li><li><a href="https://stoplight.io/blog/difference-between-open-v2-v3-v31/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">What&#8217;s the Difference Between OpenAPI 2.0, 3.0, and 3.1?</a></li></ul>



<h2 class="wp-block-heading" id="Stateoftheart:OpenAPIwithPython-Scenarios">Scenarios</h2>



<p>When you want to use OpenAPI Specification,&nbsp;you will fall into one of these three scenarios:</p>



<ul class="wp-block-list"><li>Contract-first driven API: when you start from the specification and get an API as a result</li><li>Server-first driven API: when you start from an existing or a new API and get the specification as a result</li><li>Legacy API: when you already have an API and want the OpenAPI Specification.</li></ul>



<p>In the first case, you can choose the technology you will build your API with. Many tools allow you to generate your API quickly and easily, like Swagger tools (<a href="https://swagger.io/tools/swagger-codegen/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">codegen</a>, <a href="https://swagger.io/tools/swagger-editor/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">editor</a>). The Swagger Editor even encourages you to use specific technologies to start your project with, like <a href="https://connexion.readthedocs.io" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Connexion</a> in Python. This is the more convenient way of generating an API with OpenAPI Specification.</p>



<p>In the second case, you have two possibilities:</p>



<ul class="wp-block-list"><li>starting from scratch: you can choose a technology that can generate the OpenAPI Specification</li><li>using a legacy API: you may need to adapt your code to generate the OpenAPI Specification.</li></ul>



<p>In the third case: no generation, no choice of technology, you just have to write your specification manually. 🎈<em>Easy peasy, puddin&#8217; in the freezy</em> 🎈 🎊 🧁 🐊.</p>



<p>So, two things need to be clarified when you are using Python and Flask for your API:</p>



<ul class="wp-block-list"><li>which technology to build your new API with, in order to to generate your OpenAPI Specification</li><li>which technology to generate your OpenAPI Specification with, from a legacy API.</li></ul>



<p>Here is a graph to help us to answer to those questions:</p>



<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img loading="lazy" decoding="async" src="https://www.ovh.com/blog/wp-content/uploads/2021/02/IMG_0478-1024x331.png" alt="API code (→ specification file) → documentation / Swagger UI" class="wp-image-20636" width="768" height="248" srcset="https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0478-1024x331.png 1024w, https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0478-300x97.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0478-768x248.png 768w, https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0478.png 1166w" sizes="auto, (max-width: 768px) 100vw, 768px" /></figure></div>



<p><em>Related links:</em></p>



<ul class="wp-block-list"><li><a href="https://www.openapis.org/blog/2017/09/26/three-common-scenarios-for-leveraging-the-openapi-specification" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Three common scenarios for leveraging the OpenAPI Specification</a></li><li><a href="https://engineering.zalando.com/posts/2016/12/crafting-effective-microservices-in-python.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Crafting Effective Micro-services in Python</a></li><li><a href="https://openapi.tools" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">OpenAPI Tooling</a></li><li><a href="https://swagger.io/tools/open-source/open-source-integrations/#python" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Swagger tools and integration (Python)</a></li></ul>



<h3 class="wp-block-heading" id="Stateoftheart:OpenAPIwithPython-Pythoncodetospecificationfile">Python code to specification file</h3>



<div class="wp-block-image"><figure class="alignright size-large is-resized"><img loading="lazy" decoding="async" src="https://www.ovh.com/blog/wp-content/uploads/2021/02/IMG_0474.png" alt="marshmallow" class="wp-image-20622" width="124" height="114" srcset="https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0474.png 348w, https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0474-300x277.png 300w" sizes="auto, (max-width: 124px) 100vw, 124px" /></figure></div>



<p>To get the specification file from your code, you would probably want to use docstrings. Then, one solution is to use the <a href="https://apispec.readthedocs.io" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">apispec library</a> with <a href="https://github.com/marshmallow-code/apispec/wiki/Ecosystem" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">its ecosystem</a>. It is a pluggable API specification generator with built-in support for <a href="https://marshmallow.readthedocs.io" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">marshmallow</a>. And if you are using <a href="https://github.com/alecthomas/voluptuous" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Voluptuous</a>, you can use that too <img decoding="async" src="https://docs.dsi.ovh/s/5j7bf9/8401/704793d6038510d343805f57baea5ca16b469eae/_/images/icons/emoticons/wink.svg" alt="(clin d'œil)">. And it supports OAS v2 and v3!</p>



<pre title="apispec example" class="wp-block-code"><code lang="python" class="language-python line-numbers"><code>from</code> <code>apispec import</code> <code>APISpec</code>
<code>from</code> <code>apispec_fromfile import</code> <code>FromFilePlugin</code>
&nbsp;
&nbsp;
<code># Create an APISpec</code>
<code>spec =</code> <code>APISpec(</code>
    <code>title="My Cutie Marks Catalog",</code>
    <code>version="1.0.1",</code>
    <code>openapi_version="3.0.3",</code>
    <code>plugins=[FromFilePlugin("resource")],</code>
<code>)</code>
<code>print(spec.to_yaml())</code></code></pre>



<div class="wp-block-image"><figure class="alignright size-large is-resized"><img loading="lazy" decoding="async" src="https://www.ovh.com/blog/wp-content/uploads/2021/02/IMG_0470.png" alt="Flask" class="wp-image-20626" width="250" height="125" srcset="https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0470.png 499w, https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0470-300x150.png 300w" sizes="auto, (max-width: 250px) 100vw, 250px" /></figure></div>



<p>You can use it directly, with its <a href="https://github.com/marshmallow-code/apispec-webframeworks" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">web frameworks plugin</a> and its other plugins. Or you can use Flask extensions.<br>Some frameworks are based on this library to do more things easily (and then support OAS v2 and v3):</p>



<ul class="wp-block-list"><li><a href="https://github.com/marshmallow-code/flask-smorest" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">flask-smorest</a>: based on apispec and marshmallow, use decorators a lot</li><li><a href="https://github.com/jmcarp/flask-apispec" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">flask-apispec</a>: same as flask-smorest, inspired by Flask-RESTful.</li></ul>



<p>Other frameworks are not (totally) based on it, and they often support the OpenAPI Specification v2 :</p>



<ul class="wp-block-list"><li>f<a href="https://github.com/gangverk/flask-swagger" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">lask-swagger</a> (OAS v2): as said in its description, it is &#8220;a Swagger 2.0 specification extractor for Flask&#8221;, compatible with Flask-RESTful</li><li><a href="https://github.com/flasgger/flasgger" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">flasgger</a> (OAS v2 &amp; v3): a complete fork of flask-swagger, compatible with apispec, with experimental support for OpenAPI v3.</li></ul>



<p><br>At OVHcloud, some of our APIs are using apispec with our new plugin apispec-fromfile, to avoid putting YAML into docstrings.</p>



<pre title="apispec-fromfile example" class="wp-block-code"><code lang="python" class="language-python line-numbers"><code>from</code> <code>apispec_fromfile import</code> <code>from_file</code>
<code>from</code> <code>extensions import</code> <code>spec</code>
&nbsp;
&nbsp;
<code># Create an endpoint</code>
<code>@from_file("my/spec/file.yml")</code>
<code>def</code> <code>hello():</code>
<code>return</code> <code>{"hello"}</code>
&nbsp;
<code># Register entities and paths</code>
<code>spec.path(resource=hello)</code></code></pre>



<p><em>Related links:</em></p>



<ul class="wp-block-list"><li><a href="https://dev.to/djiit/documenting-your-flask-powered-api-like-a-boss-9eo" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Documenting your Flask-powered API like a boss</a></li><li><a href="https://medium.com/analytics-vidhya/flasgger-an-api-playground-with-flask-and-swagger-ui-6b6806cf8884" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Flasgger — an API playground with Flask and Swagger UI</a></li></ul>



<h3 class="wp-block-heading" id="Stateoftheart:OpenAPIwithPython-Specificationfiletodocumentation">Specification file to documentation</h3>



<div class="wp-block-image"><figure class="alignright size-large is-resized"><img loading="lazy" decoding="async" src="https://www.ovh.com/blog/wp-content/uploads/2021/02/IMG_0475.png" alt="sphynx" class="wp-image-20624" width="288" height="80" srcset="https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0475.png 575w, https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0475-300x83.png 300w" sizes="auto, (max-width: 288px) 100vw, 288px" /></figure></div>



<p>Now that you have your specification file, in JSON or YAML format, you want to use it to describe your API. One way is to use Sphinx to generate a documentation in Python. Those extensions will help you to do that:</p>



<ul class="wp-block-list"><li><a href="https://sphinxcontrib-openapi.readthedocs.io" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">sphinxcontrib-openapi</a>: this is using&nbsp;<a href="https://sphinxcontrib-httpdomain.readthedocs.io" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">sphinxcontrib-httpdomain</a> to generate a static page</li><li><a href="https://sphinxcontrib-redoc.readthedocs.io" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">sphinxcontrib-redoc</a>: this is using <a href="https://redocly.github.io/redoc/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">ReDoc</a> to generate a more dynamic page.</li></ul>



<h3 class="wp-block-heading" id="Stateoftheart:OpenAPIwithPython-SpecificationfiletoSwaggerUI">Specification file to Swagger UI</h3>



<p>Another cool thing you can do with your specification file is to expose your API over the <a href="https://swagger.io/tools/swagger-ui/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Swagger UI</a>. This can even be used as a quick dynamic documentation. To do that, you can either spawn a Swagger UI in a container (you may need to expose your specification file) or if you are using one of the following frameworks, it is already embedded:</p>



<ul class="wp-block-list"><li><a href="https://github.com/sveint/flask-swagger-ui" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">flask-swagger-ui</a></li><li><a href="https://github.com/marshmallow-code/flask-smorest" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">flask-smorest</a></li><li><a href="https://github.com/jmcarp/flask-apispec" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">flask-apispec</a>.</li></ul>



<h3 class="wp-block-heading" id="Stateoftheart:OpenAPIwithPython-PythoncodetoSwaggerUI">Python code to Swagger UI</h3>



<p>If you choose to start with a framework, some of them can do all the graph traversal and expose an endpoint to a Swagger UI:</p>



<ul class="wp-block-list"><li><a href="https://github.com/marshmallow-code/flask-smorest" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">flask-smorest</a></li><li><a href="https://github.com/jmcarp/flask-apispec" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">flask-apispec</a></li><li><a href="https://connexion.readthedocs.io" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Connexion</a>.</li></ul>



<h3 class="wp-block-heading" id="Stateoftheart:OpenAPIwithPython-WhichtooldoIneedto^WcanIuse?">Which tool can I use?</h3>



<p>After all this reading, you may wonder which tool to use. Let&#8217;s complete the three scenarios, with some suggestions:</p>



<div class="wp-block-image"><figure class="alignright size-large is-resized"><img loading="lazy" decoding="async" src="https://www.ovh.com/blog/wp-content/uploads/2021/02/IMG_0469.png" alt="Swagger" class="wp-image-20627" width="290" height="90" srcset="https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0469.png 580w, https://blog.ovhcloud.com/wp-content/uploads/2021/02/IMG_0469-300x93.png 300w" sizes="auto, (max-width: 290px) 100vw, 290px" /></figure></div>



<ul class="wp-block-list"><li>Contract-first driven API: you can use Connexion (Swagger Editor is using it to generate Python code) or another framework</li><li>Server-first driven API: you can either start with a framework (for example flask-smorest) or complete your code with apispec or flasgger</li><li>Legacy API: you can complete your code with apispec or flasgger.</li></ul>



<p>And for the documentation, you can use Swagger UI and / or ReDoc.</p>



<p>One thing you need to pay attention to is the version of the OpenAPI Specification. It could be a good thing to start with the latest version (v3).</p>



<h3 class="wp-block-heading" id="Stateoftheart:OpenAPIwithPython-WhatisourPythonmodulefor:apispec-fromfile?">What is our Python module for: apispec-fromfile?</h3>



<p>For one our API, we were in the &#8220;server-first driven API&#8221; scenario, with an existing API based on Voluptuous and Flask. We wanted to generate the OpenAPI Specification version 3 and documentation from the code, even if we needed to adapt the code a bit.</p>



<p>Flasgger were a good starting point, with its decorator, but the support of apispec is still experimental and we are not using Marshmallow. The flask-swagger library uses a keyword in docstrings to import specification files for each endpoint, but it only supports OpenAPI v2.</p>



<p>Therefore, we kept the idea of using a decorator instead of putting YAML into docstrings, and we built an apispec plugin, which supports OpenAPI v2 and v3: <a href="https://github.com/ovh/python-apispec-fromfile/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">https://github.com/ovh/python-apispec-fromfile</a> ✨ 🍰 🎉. Then we just have to write small specification files gradually, and add a decorator to our functions.</p>



<h2 class="wp-block-heading" id="Stateoftheart:OpenAPIwithPython-Relatedlinks">Related links</h2>



<ul class="wp-block-list"><li><a href="https://github.com/p1c2u/openapi-core" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">openapi-core</a>: validation plugin</li><li><a href="https://github.com/Yelp/bravado-core" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">bravado-core</a>: same as openapi-core (openapi v2)</li><li><a href="https://github.com/mzaglia/flask-redoc" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">flask-redoc</a>: A Flask extension for displaying OpenAPI/Swagger documentation using Redoc.</li></ul>
<img loading="lazy" decoding="async" src="//blog.ovhcloud.com/wp-content/plugins/matomo/app/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fblog.ovhcloud.com%2Fopenapi-with-python-a-state-of-the-art-and-our-latest-contribution%2F&amp;action_name=OpenAPI%20with%20Python%20%E2%80%94%20a%20state%20of%20the%20art%20and%20our%20latest%20contribution&amp;urlref=https%3A%2F%2Fblog.ovhcloud.com%2Ffeed%2F" style="border:0;width:0;height:0" width="0" height="0" alt="" />]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Doing BIG automation with Celery</title>
		<link>https://blog.ovhcloud.com/doing-big-automation-with-celery/</link>
		
		<dc:creator><![CDATA[Bartosz Rabiega]]></dc:creator>
		<pubDate>Fri, 06 Mar 2020 16:14:18 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[Automation]]></category>
		<category><![CDATA[celery]]></category>
		<category><![CDATA[ceph]]></category>
		<category><![CDATA[python]]></category>
		<category><![CDATA[workflows]]></category>
		<guid isPermaLink="false">https://www.ovh.com/blog/?p=17100</guid>

					<description><![CDATA[Intro TL;DR: You might want to skip the intro and jump right into “Celery &#8211; Distributed Task Queue”. Hello! I’m Bartosz Rabiega, and I’m part of the R&#38;D/DevOps teams at OVHcloud. As part of our daily work, we’re developing and maintaining the Ceph-as-a-Service project, in order to provide highly available, solid, distributed storage for various [&#8230;]<img src="//blog.ovhcloud.com/wp-content/plugins/matomo/app/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fblog.ovhcloud.com%2Fdoing-big-automation-with-celery%2F&amp;action_name=Doing%20BIG%20automation%20with%20Celery&amp;urlref=https%3A%2F%2Fblog.ovhcloud.com%2Ffeed%2F" style="border:0;width:0;height:0" width="0" height="0" alt="" />]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">Intro</h2>



<p><strong>TL;DR</strong>: You might want to skip the intro and jump right into “Celery &#8211; Distributed Task Queue”.</p>



<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img loading="lazy" decoding="async" src="https://www.ovh.com/blog/wp-content/uploads/2020/03/2A010EF2-2666-42D4-91C1-F1FAE33148FE-1024x537.png" alt="" class="wp-image-17420" width="512" height="269" srcset="https://blog.ovhcloud.com/wp-content/uploads/2020/03/2A010EF2-2666-42D4-91C1-F1FAE33148FE-1024x537.png 1024w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/2A010EF2-2666-42D4-91C1-F1FAE33148FE-300x157.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/2A010EF2-2666-42D4-91C1-F1FAE33148FE-768x403.png 768w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/2A010EF2-2666-42D4-91C1-F1FAE33148FE.png 1200w" sizes="auto, (max-width: 512px) 100vw, 512px" /></figure></div>



<p>Hello! I’m Bartosz Rabiega, and I’m part of the R&amp;D/DevOps teams at OVHcloud. As part of our daily work, we’re developing and maintaining the Ceph-as-a-Service project, in order to provide highly available, solid, distributed storage for various applications. We’re dealing with 60PB+ of data, across 10 regions, so as you might imagine, we’ve got quite a lot of work ahead in terms of replacing broken hardware, handling natural growth, provisioning new regions and datacentres, evaluating new hardware, optimising software and hardware configurations, researching new storage solutions, and much more!</p>



<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img loading="lazy" decoding="async" src="https://www.ovh.com/blog/wp-content/uploads/2020/03/B87CD670-7779-4325-92D9-F30A1C8C71A2.png" alt="" class="wp-image-17382" width="705" height="471" srcset="https://blog.ovhcloud.com/wp-content/uploads/2020/03/B87CD670-7779-4325-92D9-F30A1C8C71A2.png 940w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/B87CD670-7779-4325-92D9-F30A1C8C71A2-300x200.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/B87CD670-7779-4325-92D9-F30A1C8C71A2-768x513.png 768w" sizes="auto, (max-width: 705px) 100vw, 705px" /></figure></div>



<p>Because of the wide scope of our work, we need to offload as many repetitive tasks as possible. And we do that through automation.</p>



<h2 class="wp-block-heading">Automating your work</h2>



<p>To some extent, every manual process can be described as set of actions and conditions. If we somehow managed to force something to automatically perform the actions and check the conditions, we would be able to automate the process, resulting in an automated workflow. Take a look at the example below, which shows some generic steps for manually replacing hardware in our project.</p>



<div class="wp-block-image"><figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="291" src="https://www.ovh.com/blog/wp-content/uploads/2020/03/E9662233-9498-4F2F-9A7E-B640F85EE295-1024x291.png" alt="" class="wp-image-17389" srcset="https://blog.ovhcloud.com/wp-content/uploads/2020/03/E9662233-9498-4F2F-9A7E-B640F85EE295-1024x291.png 1024w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/E9662233-9498-4F2F-9A7E-B640F85EE295-300x85.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/E9662233-9498-4F2F-9A7E-B640F85EE295-768x218.png 768w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/E9662233-9498-4F2F-9A7E-B640F85EE295-1536x436.png 1536w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/E9662233-9498-4F2F-9A7E-B640F85EE295.png 1677w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure></div>



<p>Hmm… What could help us do this automatically? Doesn’t a computer sound like a perfect fit? 🙂 There are many ways to force computers to process automated workflows, but first we need to define some building blocks (let’s call them tasks) and get them to run sequentially or in parallel (i.e. a workflow). Fortunately, there are software solutions that can help with that, among which is Celery.</p>



<h2 class="wp-block-heading">Celery &#8211; Distributed Task Queue</h2>



<p>Celery is a well-known and widely adopted piece of software that allows us to process tasks asynchronously. The description of the project on its main page (<a href="http://www.celeryproject.org/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">http://www.celeryproject.org/</a>) may sound a little bit enigmatic, but we can narrow down its basic functionality to something like this:</p>



<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img loading="lazy" decoding="async" src="https://www.ovh.com/blog/wp-content/uploads/2020/03/4749B2AA-AA5B-4BEF-BA3A-FC0B67FCD447-1024x539.png" alt="" class="wp-image-17414" width="768" height="404" srcset="https://blog.ovhcloud.com/wp-content/uploads/2020/03/4749B2AA-AA5B-4BEF-BA3A-FC0B67FCD447-1024x539.png 1024w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/4749B2AA-AA5B-4BEF-BA3A-FC0B67FCD447-300x158.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/4749B2AA-AA5B-4BEF-BA3A-FC0B67FCD447-768x404.png 768w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/4749B2AA-AA5B-4BEF-BA3A-FC0B67FCD447.png 1294w" sizes="auto, (max-width: 768px) 100vw, 768px" /></figure></div>



<p>Such machinery is perfectly suited to tasks like sending emails asynchronously (i.e. &#8216;fire and forget&#8217;), but it can also be used for different purposes. So what other tasks could it handle? Basically, any tasks you can implement in Python (the main Celery language)! I won’t go too much into the details, as they are available in the Celery documentation. What matters is that since we can implement any task we want, we can use that to create the building blocks for our automation.</p>



<p>There is one more important thing&#8230; Celery natively supports combining such tasks into workflows (Celery primitives: chains, groups, chords, etc.). So let’s get through some examples&#8230;</p>



<p>We’ll use the following task definitions &#8211; single task, printing <em>args</em> and <em>kwargs</em>:</p>



<pre class="wp-block-code"><code class="">@celery_app.task
def noop(*args, **kwargs):
    # Task accepts any arguments and does nothing
    print(args, kwargs)
    return True</code></pre>



<p>Now we can execute the task asynchronously, using the following code:</p>



<pre class="wp-block-code"><code class="">task = noop.s(777)
task.apply_async()</code></pre>



<p>The elementary tasks can be parametrised and combined into a complex workflow using celery methods, i.e. “chain”, “group”, and “chord”. See the examples below. In each of them, the left side shows a visual representation of a workflow, while the right side shows the code snippet that generates it. The green box is the starting point, after which the workflow execution progresses vertically.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-flow wp-block-group-is-layout-flow">
<h4 class="wp-block-heading">Chain &#8211; a set of tasks processed sequentially</h4>



<div class="wp-block-columns is-layout-flex wp-container-core-columns-is-layout-28f84493 wp-block-columns-is-layout-flex">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img loading="lazy" decoding="async" src="https://www.ovh.com/blog/wp-content/uploads/2020/03/705AD975-048B-4E6A-8BFF-F68775C9C5C7.png" alt="" class="wp-image-17394" width="92" height="320"/></figure></div>
</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<pre class="wp-block-code"><code class="">workflow = (
    chain([noop.s(i) for i in range(3)])
)</code></pre>
</div>
</div>



<h4 class="wp-block-heading">Group &#8211; a set of tasks processed in parallel</h4>



<div class="wp-block-columns is-layout-flex wp-container-core-columns-is-layout-28f84493 wp-block-columns-is-layout-flex">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" src="https://www.ovh.com/blog/wp-content/uploads/2020/03/B112B87E-2813-46DD-9105-4B528BB3C110.png" alt="" class="wp-image-17396" width="317" height="169" srcset="https://blog.ovhcloud.com/wp-content/uploads/2020/03/B112B87E-2813-46DD-9105-4B528BB3C110.png 633w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/B112B87E-2813-46DD-9105-4B528BB3C110-300x160.png 300w" sizes="auto, (max-width: 317px) 100vw, 317px" /></figure>
</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<pre class="wp-block-code"><code class="">workflow = (
    group([noop.s(i) for i in range(5)])
)</code></pre>
</div>
</div>



<h4 class="wp-block-heading">Chord &#8211; a group of tasks chained to the following task</h4>



<div class="wp-block-columns is-layout-flex wp-container-core-columns-is-layout-28f84493 wp-block-columns-is-layout-flex">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" src="https://www.ovh.com/blog/wp-content/uploads/2020/03/4E75C373-2CE1-4A68-8599-245E768167A4.png" alt="" class="wp-image-17397" width="311" height="223" srcset="https://blog.ovhcloud.com/wp-content/uploads/2020/03/4E75C373-2CE1-4A68-8599-245E768167A4.png 621w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/4E75C373-2CE1-4A68-8599-245E768167A4-300x215.png 300w" sizes="auto, (max-width: 311px) 100vw, 311px" /></figure>
</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<pre class="wp-block-code"><code class="">workflow = chord(
        [noop.s(i) for i in range(5)],
        noop.s(i)
)

# Equivalent:
workflow = chain([
        group([noop.s(i) for i in range(5)]),
        noop.s(i)
])</code></pre>
</div>
</div>
</div></div>



<p>An important point: the execution of a workflow will always stop in the event of a failed task. As a result, a chain won’t be continued if some task fails in the middle of it. This gives us quite a powerful framework for implementing some neat automation, and that’s exactly what we’re using for Ceph-as-a-Service at OVHcloud! We’ve implemented lots of small, flexible, parameterisable tasks, which we combine together to reach a common goal. Here are some real-life examples of elementary tasks, used for the automatic removal of old hardware:</p>



<ul class="wp-block-list"><li>Change weight of Ceph node (used to increase/decrease the amount of data on node. Triggers data rebalance)</li><li>Set service downtime (data rebalance triggers monitoring probes, but this is expected, so set downtime for this particular monitoring entry)</li><li>Wait until Ceph is healthy (wait until the data rebalance is complete &#8211; repeating task)</li><li>Remove Ceph node from a cluster (node is empty so it can simply be uninstalled)</li><li>Send info to technicians in DC (hardware is ready to be replaced)</li><li>Add new Ceph node to a cluster (install new empty node)</li></ul>



<p>We parametrise these tasks and tie them together, using Celery chains, groups and chords to create the desired workflow. Celery then does the rest by asynchronously executing the workflow.</p>



<h2 class="wp-block-heading">Big workflows and Celery</h2>



<p>As our infrastructure grows, so doo our automated workflows grow, with more tasks per workflow, higher complexity of workflows&#8230; What do we understand as a big workflow? A workflow consisting of 1,000-10,000 tasks. Just to visualize it take a look on following examples:</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-flow wp-block-group-is-layout-flow">
<h4 class="wp-block-heading">A few chords chained together (57 tasks in total)</h4>



<div class="wp-block-columns is-layout-flex wp-container-core-columns-is-layout-28f84493 wp-block-columns-is-layout-flex">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<div class="wp-block-image"><figure class="aligncenter"><img decoding="async" src="https://lh4.googleusercontent.com/XZWOfqmSMu68u7GcbvceB0mc8_HA_v8higDeoG08dlO5oTlRd9R98QBSlf4sMLPuiFB2RPVgM-6i7vG86jtAxMCrKSLTkt0nK4z5JSbYE4QkXF96qkXh3uSJYj1X82UUm-agBMxu" alt=""/></figure></div>
</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<pre class="wp-block-code"><code class="">workflow = chain([
    noop.s(0),
    chord([noop.s(i) for i in range(10)], noop.s()),
    chord([noop.s(i) for i in range(10)], noop.s()),
    chord([noop.s(i) for i in range(10)], noop.s()),
    chord([noop.s(i) for i in range(10)], noop.s()),
    chord([noop.s(i) for i in range(10)], noop.s()),
    noop.s()
])</code></pre>
</div>
</div>



<h4 class="wp-block-heading">More complex graph structure built from chains and groups (23 tasks in total)</h4>



<div class="wp-block-columns is-layout-flex wp-container-core-columns-is-layout-28f84493 wp-block-columns-is-layout-flex">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<div class="wp-block-image"><figure class="aligncenter"><img decoding="async" src="https://lh5.googleusercontent.com/gUQlIa5Nmb4a5oNDbojhBtukEn--6dSxlKrn-enggXk9eCtuBvgVBTxecwAczOMghEoZ0zOtKuz0nohZTsj01QqVBxkbX8bxqyVVvYjC6B1sfrpXN8pferDSgg-RE6TB6v5SOBdL" alt=""/></figure></div>
</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<pre class="wp-block-code"><code class=""># | is ‘chain’ operator in celery
workflow = (
    group(
        group(
            group([noop.s() for i in range(5)]),
            chain([noop.s() for i in range(5)])
        ) |
        noop.s() |
        group([noop.s() for i in range(5)]) |
        noop.s(),
        chain([noop.s() for i in range(5)])
    ) |
    noop.s()
)</code></pre>
</div>
</div>
</div></div>



<p>As you can probably imagine, visualisations get quite big and messy when 1,000 tasks are involved! Celery is a powerful tool, and has lots of features that are well-suited for automation, but it still struggles when it comes to processing big, complex, long-running workflows. Orchestrating the execution of 10,000 tasks, with a variety of dependencies, is no trivial thing. There are several issues we encountered when our automation grew too big:</p>



<ul class="wp-block-list"><li>Memory issues during workflow building (client side)</li><li>Serialisation issues (client -&gt; Celery backend transfer)</li><li>Nondeterministic, broken execution of workflows</li><li>Memory issues in Celery workers (Celery backend)</li><li>Disappearing tasks</li><li>And more&#8230;</li></ul>



<p>Take a look at some GitHub tickets:</p>



<ul class="wp-block-list"><li><a href="https://github.com/celery/celery/issues/5000" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">https://github.com/celery/celery/issues/5000</a></li><li><a href="https://github.com/celery/celery/issues/5286" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">https://github.com/celery/celery/issues/5286</a></li><li><a href="https://github.com/celery/celery/issues/5327" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">https://github.com/celery/celery/issues/5327</a></li><li><a href="https://github.com/celery/celery/issues/3723" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">https://github.com/celery/celery/issues/3723</a></li></ul>



<p>Using Celery for our particular use case became difficult and unreliable. Celery’s native support for workflows doesn’t seem to be the right choice for handling 100/1,000/10,000 tasks. In its current state, it’s just not enough. So here we stand, in front of a solid, concrete wall… Either we somehow fix Celery, or we rewrite our automation using a different framework.</p>



<h2 class="wp-block-heading">Celery &#8211; to fix&#8230; or to fix?</h2>



<p>Rewriting all of our automation would be possible, although relatively painful. Since I’m a rather lazy person, perhaps attempting to fix Celery wasn’t an entirely bad idea? So I took some time to dig through Celery’s code, and managed to find the parts responsible for building workflows, and executing chains and chords. It was still a little bit difficult for me to understand all the different code paths handling the wide range of use cases, but I realised it would be possible to implement a clean, straightforward orchestration that would handle all the tasks and their combinations in the same way. What’s more, I had a glimpse that it wouldn&#8217;t take too much effort to integrate it into our automation (let’s not forget the main goal!). </p>



<p>Unfortunately, introducing new orchestration into the Celery project would probably be quite hard, and would most likely break some backwards compatibility. So I decided to take a different approach &#8211; writing an extension or a plugin that wouldn’t require changes in Celery. Something pluggable, and as non-invasive as possible. That’s how Celery Dyrygent emerged&#8230;</p>



<h2 class="wp-block-heading">Celery Dyrygent</h2>



<p><a href="https://github.com/ovh/celery-dyrygent" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">https://github.com/ovh/celery-dyrygent</a></p>



<h3 class="wp-block-heading">How to represent a workflow</h3>



<p>You can think of a workflow as a directed acyclic graph (DAG), where each task is a separate graph node. When it comes to acyclic graphs, it is relatively easy to store and resolve dependencies between nodes, which leads to straightforward orchestration. Celery Dyrygent was implemented based on these features. Each task in the workflow has an unique identifier (Celery already assigns task IDs when a task is pushed for execution) and each one of them is wrapped into a workflow node. Each workflow node consists of a task signature (a plain Celery signature) and a list of IDs for the tasks it depends on. See the example below:</p>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" src="https://www.ovh.com/blog/wp-content/uploads/2020/03/F4601B45-EB13-4710-9325-B9684BF77918-1024x533.png" alt="" class="wp-image-17400" width="512" height="267" srcset="https://blog.ovhcloud.com/wp-content/uploads/2020/03/F4601B45-EB13-4710-9325-B9684BF77918-1024x533.png 1024w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/F4601B45-EB13-4710-9325-B9684BF77918-300x156.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/F4601B45-EB13-4710-9325-B9684BF77918-768x400.png 768w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/F4601B45-EB13-4710-9325-B9684BF77918.png 1172w" sizes="auto, (max-width: 512px) 100vw, 512px" /></figure>



<h3 class="wp-block-heading">How to process a workflow</h3>



<p>So we know how to store a workflow in a clean and easy way. Now we just need to execute it. How about using&#8230; Celery? Why not? For this, Celery Dyrygent introduces a <strong>workflow processor</strong> task (an ordinary Celery task). This task wraps a whole workflow and schedules an execution of primitive tasks, according to their dependencies. Once the scheduling part is over, the task repeats itself (it &#8216;ticks&#8217; with some delay). </p>



<p>Throughout the whole processing cycle, workflow processor retains the state of the entire workflow internally. As a result, it updates the state with each repetition. You can see an orchestration example below:</p>



<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img loading="lazy" decoding="async" src="https://www.ovh.com/blog/wp-content/uploads/2020/03/CE6EE688-92F2-4BA5-9A6B-147BD956A0F0-1024x553.png" alt="" class="wp-image-17416" width="512" height="277" srcset="https://blog.ovhcloud.com/wp-content/uploads/2020/03/CE6EE688-92F2-4BA5-9A6B-147BD956A0F0-1024x553.png 1024w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/CE6EE688-92F2-4BA5-9A6B-147BD956A0F0-300x162.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/CE6EE688-92F2-4BA5-9A6B-147BD956A0F0-768x415.png 768w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/CE6EE688-92F2-4BA5-9A6B-147BD956A0F0.png 1470w" sizes="auto, (max-width: 512px) 100vw, 512px" /></figure></div>



<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img loading="lazy" decoding="async" src="https://www.ovh.com/blog/wp-content/uploads/2020/03/7764C3D5-1EF9-44A9-A588-4C37A275570B-1024x553.png" alt="" class="wp-image-17417" width="512" height="277" srcset="https://blog.ovhcloud.com/wp-content/uploads/2020/03/7764C3D5-1EF9-44A9-A588-4C37A275570B-1024x553.png 1024w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/7764C3D5-1EF9-44A9-A588-4C37A275570B-300x162.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/7764C3D5-1EF9-44A9-A588-4C37A275570B-768x415.png 768w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/7764C3D5-1EF9-44A9-A588-4C37A275570B.png 1470w" sizes="auto, (max-width: 512px) 100vw, 512px" /></figure></div>



<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img loading="lazy" decoding="async" src="https://www.ovh.com/blog/wp-content/uploads/2020/03/F2E6717E-B355-46AB-AD73-6C98B6CE4B19-1024x553.png" alt="" class="wp-image-17418" width="512" height="277" srcset="https://blog.ovhcloud.com/wp-content/uploads/2020/03/F2E6717E-B355-46AB-AD73-6C98B6CE4B19-1024x553.png 1024w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/F2E6717E-B355-46AB-AD73-6C98B6CE4B19-300x162.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/F2E6717E-B355-46AB-AD73-6C98B6CE4B19-768x415.png 768w, https://blog.ovhcloud.com/wp-content/uploads/2020/03/F2E6717E-B355-46AB-AD73-6C98B6CE4B19.png 1470w" sizes="auto, (max-width: 512px) 100vw, 512px" /></figure></div>



<p>Most notably, workflow processor stops its execution in two cases:</p>



<ul class="wp-block-list"><li>Once the whole workflow finishes, with all tasks successfully completed</li><li>When it can’t proceed any further, due to a failed task</li></ul>



<h3 class="wp-block-heading">How to integrate</h3>



<p>So how do we use this? Fortunately, I was able to find a way to use Celery Dyrygent quite easily. First of all, you need to inject the workflow processor task definition into your Celery applicationP:</p>



<pre class="wp-block-code"><code class="">from celery_dyrygent.tasks import register_workflow_processor
app = Celery() #  your celery application instance
workflow_processor = register_workflow_processor(app)</code></pre>



<p>Next, you need to convert your Celery defined workflow into a Celery Dyrygent workflow:</p>



<pre class="wp-block-code"><code class="">from celery_dyrygent.workflows import Workflow

celery_workflow = chain([
    noop.s(0),
    chord([noop.s(i) for i in range(10)], noop.s()),
    chord([noop.s(i) for i in range(10)], noop.s()),
    chord([noop.s(i) for i in range(10)], noop.s()),
    chord([noop.s(i) for i in range(10)], noop.s()),
    chord([noop.s(i) for i in range(10)], noop.s()),
    noop.s()
])

workflow = Workflow()
workflow.add_celery_canvas(celery_workflow)</code></pre>



<p>Finally, simply execute the workflow, just as you would an ordinary Celery task:</p>



<pre class="wp-block-code"><code class="">workflow.apply_async()</code></pre>



<p>That’s it! You can always go back if you wish, as the small changes are very easy to undo.</p>



<h3 class="wp-block-heading">Give it a try!</h3>



<p>Celery Dyrygent is free to use, and its source code is available on Github (<a href="https://github.com/ovh/celery-dyrygent" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">https://github.com/ovh/celery-dyrygent</a>). Feel free to use it, improve it, request features, and report any bugs! It has a few additional features not described here, so I&#8217;d encourage you to take a look at the project’s readme file. For our automation requirements, it&#8217;s already a solid, battle-tested solution. We’ve been using it since the end of 2018, and it has processed thousands of workflows, consisting of hundreds of thousands of tasks. Here are some productions stats, from June 2019 to February 2020:</p>



<ul class="wp-block-list"><li>936,248 elementary tasks executed</li><li>11,170 workflows processed</li><li>4,098 tasks in the biggest workflow so far</li><li>~84 tasks per workflow, on average</li></ul>



<p>Automation is always a good idea!</p>
<img loading="lazy" decoding="async" src="//blog.ovhcloud.com/wp-content/plugins/matomo/app/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fblog.ovhcloud.com%2Fdoing-big-automation-with-celery%2F&amp;action_name=Doing%20BIG%20automation%20with%20Celery&amp;urlref=https%3A%2F%2Fblog.ovhcloud.com%2Ffeed%2F" style="border:0;width:0;height:0" width="0" height="0" alt="" />]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Introducing Director – a tool to build your Celery workflows</title>
		<link>https://blog.ovhcloud.com/introducing-director-a-tool-to-build-your-celery-workflows/</link>
		
		<dc:creator><![CDATA[Nicolas Crocfer]]></dc:creator>
		<pubDate>Wed, 26 Feb 2020 12:38:57 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[Automation]]></category>
		<category><![CDATA[celery]]></category>
		<category><![CDATA[Open Source]]></category>
		<category><![CDATA[python]]></category>
		<guid isPermaLink="false">https://www.ovh.com/blog/?p=17064</guid>

					<description><![CDATA[As developers, we often need to execute tasks in the background. Fortunately, some tools already exist for this. In the Python ecosystem, for instance, the most well-known library is Celery. If you have already used it, you know how great it is! But you will also have probably discovered how complicated it can be to [&#8230;]<img src="//blog.ovhcloud.com/wp-content/plugins/matomo/app/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fblog.ovhcloud.com%2Fintroducing-director-a-tool-to-build-your-celery-workflows%2F&amp;action_name=Introducing%20Director%20%E2%80%93%20a%20tool%20to%20build%20your%20Celery%20workflows&amp;urlref=https%3A%2F%2Fblog.ovhcloud.com%2Ffeed%2F" style="border:0;width:0;height:0" width="0" height="0" alt="" />]]></description>
										<content:encoded><![CDATA[
<p>As developers, we often need to execute tasks in the background. Fortunately, some tools already exist for this. In the Python ecosystem, for instance, the most well-known library is <a href="http://www.celeryproject.org/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Celery</a>. If you have already used it, you know how great it is! But you will also have probably discovered how complicated it can be to follow the state of a complex workflow.</p>



<div class="wp-block-image"><figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="537" src="https://www.ovh.com/blog/wp-content/uploads/2020/02/7E201458-960D-44E8-8DF8-816CE1DE766E-1024x537.jpeg" alt="" class="wp-image-17224" srcset="https://blog.ovhcloud.com/wp-content/uploads/2020/02/7E201458-960D-44E8-8DF8-816CE1DE766E-1024x537.jpeg 1024w, https://blog.ovhcloud.com/wp-content/uploads/2020/02/7E201458-960D-44E8-8DF8-816CE1DE766E-300x157.jpeg 300w, https://blog.ovhcloud.com/wp-content/uploads/2020/02/7E201458-960D-44E8-8DF8-816CE1DE766E-768x403.jpeg 768w, https://blog.ovhcloud.com/wp-content/uploads/2020/02/7E201458-960D-44E8-8DF8-816CE1DE766E.jpeg 1200w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure></div>



<p><strong>Celery Director</strong> is a tool we created at OVHcloud to fix this problem. The code is now open-sourced and is available on <a href="https://github.com/ovh/celery-director" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Github</a>.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="525" src="https://www.ovh.com/blog/wp-content/uploads/2020/02/director-1024x525.png" alt="" class="wp-image-17098" srcset="https://blog.ovhcloud.com/wp-content/uploads/2020/02/director-1024x525.png 1024w, https://blog.ovhcloud.com/wp-content/uploads/2020/02/director-300x154.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2020/02/director-768x394.png 768w, https://blog.ovhcloud.com/wp-content/uploads/2020/02/director.png 1440w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>Following the talk we did during <a href="https://fosdem.org/2020/schedule/event/python2020_celery/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">FOSDEM 2020</a>, this post aims to present the tool. We&#8217;ll take a close look at what Celery is, why we created Director, and how to use it.</p>



<h2 class="wp-block-heading">What is Celery?</h2>



<p>Here is the official description of Celery:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow"><p>Celery is an asynchronous <strong>task queue</strong>/job queue based on distributed message passing. It is focused on real-time operation, but supports scheduling as well.</p></blockquote>



<p>The important words here are &#8220;task queue&#8221;. This is a mechanism used to distribute work across a pool of machines or threads.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="572" src="https://www.ovh.com/blog/wp-content/uploads/2020/02/51EA37AB-E3E5-453F-9EFD-92414C84523F-1024x572.jpeg" alt="" class="wp-image-17220" srcset="https://blog.ovhcloud.com/wp-content/uploads/2020/02/51EA37AB-E3E5-453F-9EFD-92414C84523F-1024x572.jpeg 1024w, https://blog.ovhcloud.com/wp-content/uploads/2020/02/51EA37AB-E3E5-453F-9EFD-92414C84523F-300x168.jpeg 300w, https://blog.ovhcloud.com/wp-content/uploads/2020/02/51EA37AB-E3E5-453F-9EFD-92414C84523F-768x429.jpeg 768w, https://blog.ovhcloud.com/wp-content/uploads/2020/02/51EA37AB-E3E5-453F-9EFD-92414C84523F.jpeg 1156w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>The queue, in the middle of the above diagram, stores messages sent by the producers (APIs, for instance). On the other side, consumers are constantly reading the queue to display new messages and execute tasks.</p>



<p>In Celery, a message sent by the producer is the signature of a Python function: <code>send_email("john.doe")</code>, for example.</p>



<p>The queue (named <em>broker</em> in Celery) stores this signature until a worker reads it and <strong>really</strong> executes the function within the given parameter.</p>



<p>But why execute a Python function <em>somewhere else</em>? The main reason is to quickly return a response in cases of long-running functions. Indeed, it&#8217;s not an option to keep users waiting for a response for several seconds or minutes. </p>



<p>Just as we can imagine producers without enough resources, with a CPU-bound task, a more robust worker could handle its execution.</p>



<h2 class="wp-block-heading">How to use Celery</h2>



<p>So Celery is a library used to execute a Python code <em>somewhere else</em>, but how does it do that? In fact, it&#8217;s really simple! To illustrate this, we&#8217;ll use some of the available methods to send tasks to the broker, then we&#8217;ll start a worker to consume them.</p>



<p>Here is the code to create a Celery task:</p>



<pre class="wp-block-code"><code class=""># tasks.py
from celery import Celery

app = Celery("tasks", broker="redis://127.0.0.1:6379/0")

@app.task
def add(x, y):
    return x + y</code></pre>



<p>As you can see, a Celery task is just a Python function transformed to be sent in a broker. Note that we passed the redis connection to the Celery application (named app) to inform the broker where to store the messages.</p>



<p>This means it&#8217;s now possible to send a task in the broker:</p>



<pre class="wp-block-code"><code class="">>>> from tasks import add
>>> add.delay(2, 3)</code></pre>



<p>That&#8217;s all! We used the <code>.delay()</code> method, so our producer didn&#8217;t execute the Python code but instead sent the task signature to the broker.</p>



<p>Now it&#8217;s time to consume it with a Celery worker:</p>



<pre class="wp-block-code"><code class="">$ celery worker -A tasks --loglevel=INFO
[...]
[2020-02-14 17:13:38,947: INFO/MainProcess] Received task: tasks.add[0e9b6ff2-7aec-46c3-b810-b62a32188000]
[2020-02-14 17:13:38,954: INFO/ForkPoolWorker-2] Task tasks.add[0e9b6ff2-7aec-46c3-b810-b62a32188000] succeeded in 0.0024250600254163146s: 5</code></pre>



<p>It&#8217;s even possible to combine the Celery tasks with some primitives (the full list is <a href="https://docs.celeryproject.org/en/stable/userguide/canvas.html#the-primitives" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">here</a>):</p>



<ul class="wp-block-list"><li>Chain: will execute tasks one after the other.</li><li>Group: will execute tasks in parallel by routing them to multiple workers.</li></ul>



<p>For example, the following code will make two additions in parallel, then sum the results:</p>



<pre class="wp-block-code"><code class="">from celery import chain, group

# Create the canvas
canvas = chain(
    group(
        add.si(1, 2),
        add.si(3, 4)
    ),
    sum_numbers.s()
)

# Execute it
canvas.delay()</code></pre>



<p>You probably noted we didn&#8217;t use the <em>.delay()</em> method here. Instead we created a <strong>canvas</strong>, used to combine a selection of tasks.</p>



<p>The <code>.si()</code> method is used to create an immutable signature (i.e. one that does not receive data from a previous task), while <code>.s()</code> relies on the data returned by the two previous tasks.</p>



<p>This introduction to Celery has just covered its very basic usage. If you&#8217;re keen to find out more, I invite you to read the documentation, where you&#8217;ll discover all the powerful features, including <strong>rate limits</strong>, <strong>tasks retrying</strong>, or even <strong>periodic tasks</strong>. </p>



<h2 class="wp-block-heading">As a developer, I want&#8230;</h2>



<p>I&#8217;m part of a team whose goal is to deploy and monitor internal infrastructures. As part of this, we needed to launch some background tasks, and as Python developers our natural choice was to use Celery. But, out of the box, Celery didn&#8217;t supported certain specific requirements for our projects:</p>



<ul class="wp-block-list"><li>Tracking the tasks&#8217; evolution and their dependencies in a WebUI.</li><li>Executing the workflows using API calls, or simply with a CLI.</li><li>Combining tasks to create workflows in YAML format.</li><li>Periodically executing a whole workflow.</li></ul>



<p>Some other cool tools exist for this, like Flower, but this only allows us to track each task individually, not a whole workflow and its component tasks.</p>



<p>And as we really needed these features, we decided to create <a href="https://github.com/ovh/celery-director" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Celery Director</a>.</p>



<div class="wp-block-image"><figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="377" height="377" src="https://www.ovh.com/blog/wp-content/uploads/2020/02/2E75457D-256F-4CB9-942B-B1B8C00CF79B.png" alt="" class="wp-image-17222" srcset="https://blog.ovhcloud.com/wp-content/uploads/2020/02/2E75457D-256F-4CB9-942B-B1B8C00CF79B.png 377w, https://blog.ovhcloud.com/wp-content/uploads/2020/02/2E75457D-256F-4CB9-942B-B1B8C00CF79B-300x300.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2020/02/2E75457D-256F-4CB9-942B-B1B8C00CF79B-150x150.png 150w" sizes="auto, (max-width: 377px) 100vw, 377px" /></figure></div>



<h2 class="wp-block-heading">How to use Director</h2>



<p>The installation can be done using the <code>pip</code>command:</p>



<pre class="wp-block-code"><code class="">$ pip install celery-director</code></pre>



<p>Director provides a simple command to create a new workspace folder:</p>



<pre class="wp-block-code"><code class="">$ director init workflows
[*] Project created in /home/ncrocfer/workflows
[*] Do not forget to initialize the database
You can now export the DIRECTOR_HOME environment variable</code></pre>



<p>A new tasks folder and a workflow example has been created for you below:</p>



<pre class="wp-block-code"><code class="">$ tree -a workflows/
├── .env
├── tasks
│   └── etl.py
└── workflows.yml</code></pre>



<p>The <code>tasks/*.py</code> files will contain your Celery tasks, while the <code>workflows.yml</code> file will combine them:</p>



<pre class="wp-block-code"><code class="">$ cat workflows.yml
---
ovh.SIMPLE_ETL:
  tasks:
    - EXTRACT
    - TRANSFORM
    - LOAD</code></pre>



<p>This example, named <strong>ovh.SIMPLE_ETL</strong>, will execute three tasks, one after the other. You can find more examples in the <a href="https://ovh.github.io/celery-director/guides/build-workflows/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">documentation</a>.</p>



<p>After exporting the <code>DIRECTOR_HOME</code> variable and initialising the database with <code>director db upgrade</code>, you can execute this workflow :</p>



<pre class="wp-block-code"><code class="">$ director workflow list
+----------------+----------+-----------+
| Workflows (1)  | Periodic | Tasks     |
+----------------+----------+-----------+
| ovh.SIMPLE_ETL |    --    | EXTRACT   |
|                |          | TRANSFORM |
|                |          | LOAD      |
+----------------+----------+-----------+
$ director workflow run ovh.SIMPLE_ETL</code></pre>



<p>The broker has received the tasks, so now you can launch the Celery worker to execute them:</p>



<pre class="wp-block-code"><code class="">$ director celery worker --loglevel=INFO</code></pre>



<p>And then display the results using the webserver command (<code>director webserver</code>):</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="530" src="https://www.ovh.com/blog/wp-content/uploads/2020/02/director_etl-1024x530.png" alt="" class="wp-image-17094" srcset="https://blog.ovhcloud.com/wp-content/uploads/2020/02/director_etl-1024x530.png 1024w, https://blog.ovhcloud.com/wp-content/uploads/2020/02/director_etl-300x155.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2020/02/director_etl-768x397.png 768w, https://blog.ovhcloud.com/wp-content/uploads/2020/02/director_etl.png 1440w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>This is just the beginning, as Director provides other features, allowing you to parametrise a workflow or periodically execute it, for example. You will find more details on these features in the <a href="https://ovh.github.io/celery-director/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">documentation</a>.</p>



<h2 class="wp-block-heading">Conclusion</h2>



<p>Our teams use Director regularly to launch our workflows. No more boilerplating, and no more need for advanced Celery knowledge&#8230; A new colleague can easily create its tasks in Python and combine them in YAML, without using the Celery primitives discussed earlier.</p>



<p>Sometimes we need to execute a workflow periodically (to populate a cache, for instance), and sometimes we need to manually call it from another web service (note that a workflow can also be executed through an <a href="https://ovh.github.io/celery-director/guides/run-workflows/#using-the-api" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">API call</a>). This is now possible using our single Director instance.</p>



<p>We invite you to try Director for yourself, and give us your feedback via Github, so we can continue to enhance it. The source code can be found in <a href="https://github.com/ovh/celery-director" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Github</a>, and the 2020 FOSDEM presentation is available <a href="https://fosdem.org/2020/schedule/event/python2020_celery/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">here</a>.</p>
<img loading="lazy" decoding="async" src="//blog.ovhcloud.com/wp-content/plugins/matomo/app/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fblog.ovhcloud.com%2Fintroducing-director-a-tool-to-build-your-celery-workflows%2F&amp;action_name=Introducing%20Director%20%E2%80%93%20a%20tool%20to%20build%20your%20Celery%20workflows&amp;urlref=https%3A%2F%2Fblog.ovhcloud.com%2Ffeed%2F" style="border:0;width:0;height:0" width="0" height="0" alt="" />]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
