]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
:art: Update terminal examples and Typer note (#1139)
authorSebastián Ramírez <tiangolo@gmail.com>
Thu, 19 Mar 2020 13:39:29 +0000 (14:39 +0100)
committerGitHub <noreply@github.com>
Thu, 19 Mar 2020 13:39:29 +0000 (14:39 +0100)
* :art: Update terminal examples with Termynal

* :bento: Add Termynal scripts and styles from Typer for terminal examples

22 files changed:
README.md
docs/advanced/extending-openapi.md
docs/advanced/sql-databases-peewee.md
docs/advanced/sub-applications-proxy.md
docs/advanced/templates.md
docs/advanced/websockets.md
docs/contributing.md
docs/css/termynal.css [new file with mode: 0644]
docs/deployment.md
docs/index.md
docs/js/custom.js
docs/js/termynal.js [new file with mode: 0644]
docs/tutorial/bigger-applications.md
docs/tutorial/debugging.md
docs/tutorial/first-steps.md
docs/tutorial/index.md
docs/tutorial/security/first-steps.md
docs/tutorial/security/oauth2-jwt.md
docs/tutorial/sql-databases.md
docs/tutorial/static-files.md
docs/tutorial/testing.md
mkdocs.yml

index 8801c1af57b63965240088b49e4d155a9b588c26..11c9453720dc7b52eea625cd20063b4c93da0564 100644 (file)
--- a/README.md
+++ b/README.md
@@ -77,6 +77,14 @@ The key features are:
 
 ---
 
+## **Typer**, the FastAPI of CLIs
+
+<a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a>
+
+If you are building a <abbr title="Command Line Interface">CLI</abbr> app to be used in the terminal instead of a web API, check out <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>.
+
+**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀
+
 ## Requirements
 
 Python 3.6+
@@ -88,16 +96,28 @@ FastAPI stands on the shoulders of giants:
 
 ## Installation
 
-```bash
-pip install fastapi
+<div class="termy">
+
+```console
+$ pip install fastapi
+
+---> 100%
 ```
 
+</div>
+
 You will also need an ASGI server, for production such as <a href="http://www.uvicorn.org" class="external-link" target="_blank">Uvicorn</a> or <a href="https://gitlab.com/pgjones/hypercorn" class="external-link" target="_blank">Hypercorn</a>.
 
-```bash
-pip install uvicorn
+<div class="termy">
+
+```console
+$ pip install uvicorn
+
+---> 100%
 ```
 
+</div>
+
 ## Example
 
 ### Create it
@@ -151,10 +171,20 @@ If you don't know, check the _"In a hurry?"_ section about <a href="https://fast
 
 Run the server with:
 
-```bash
-uvicorn main:app --reload
+<div class="termy">
+
+```console
+$ uvicorn main:app --reload
+
+<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
+<span style="color: green;">INFO</span>:     Started reloader process [28720]
+<span style="color: green;">INFO</span>:     Started server process [28722]
+<span style="color: green;">INFO</span>:     Waiting for application startup.
+<span style="color: green;">INFO</span>:     Application startup complete.
 ```
 
+</div>
+
 <details markdown="1">
 <summary>About the command <code>uvicorn main:app --reload</code>...</summary>
 
index 91fb29db874663a93ff1de47cc4de6319f950c29..4841c92134d941c0244c82cdb693ed2a7d8f832e 100644 (file)
@@ -155,10 +155,17 @@ After that, your file structure could look like:
 
 Now you need to install `aiofiles`:
 
-```bash
-pip install aiofiles
+
+<div class="termy">
+
+```console
+$ pip install aiofiles
+
+---> 100%
 ```
 
+</div>
+
 ### Serve the static files
 
 * Import `StaticFiles`.
index 4c05daf7710f84590a66c989667d02c0b37eb57b..ae957bec640c3daeec2ecc3da586bac159f519f1 100644 (file)
@@ -369,10 +369,16 @@ async def reset_db_state():
 
 Then run your app with Uvicorn:
 
-```bash
-uvicorn sql_app.main:app --reload
+<div class="termy">
+
+```console
+$ uvicorn sql_app.main:app --reload
+
+<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
 ```
 
+</div>
+
 Open your browser at <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>  and create a couple of users.
 
 Then open 10 tabs at <a href="http://127.0.0.1:8000/docs#/default/read_slow_users_slowusers__get" class="external-link" target="_blank">http://127.0.0.1:8000/docs#/default/read_slow_users_slowusers__get</a> at the same time.
index e75a4e9d3aaf813724c869fb10ad222c815342fa..333ef9ae2de76cdc12dea88e27ab3fdb9693c2b0 100644 (file)
@@ -73,10 +73,16 @@ Here you need to make sure you use the same path that you used for the `openapi_
 
 Now, run `uvicorn`, if your file is at `main.py`, it would be:
 
-```bash
-uvicorn main:app --reload
+<div class="termy">
+
+```console
+$ uvicorn main:app --reload
+
+<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
 ```
 
+</div>
+
 And open the docs at <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>.
 
 You will see the automatic API docs for the main app, including only its own paths:
index 9b8f750b5923c95d1399bcaebc3766535c100965..da4752e4a598ddb0a58205ef6f1aad1f3ff16179 100644 (file)
@@ -8,16 +8,28 @@ There are utilities to configure it easily that you can use directly in your **F
 
 Install `jinja2`:
 
-```bash
-pip install jinja2
+<div class="termy">
+
+```console
+$ pip install jinja2
+
+---> 100%
 ```
 
+</div>
+
 If you need to also serve static files (as in this example), install `aiofiles`:
 
-```bash
-pip install aiofiles
+<div class="termy">
+
+```console
+$ pip install aiofiles
+
+---> 100%
 ```
 
+</div>
+
 ## Using `Jinja2Templates`
 
 * Import `Jinja2Templates`.
index ac6d93864ff3ca7b2da5986abe9edef3adf96e3c..a76eab59c815e07d89ec01beb6f386d377203a1a 100644 (file)
@@ -85,10 +85,16 @@ To learn more about the options, check Starlette's documentation for:
 
 If your file is named `main.py`, run your application with:
 
-```bash
-uvicorn main:app --reload
+<div class="termy">
+
+```console
+$ uvicorn main:app --reload
+
+<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
 ```
 
+</div>
+
 Open your browser at <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>.
 
 You will see a simple page like:
index 9cf663605bc8582f637b72d29f3ceab7452f594d..4ca50db9e831a16f3ef06e687a51e2337f156505 100644 (file)
@@ -51,6 +51,7 @@ $ Get-Command pip
 
 some/directory/fastapi/env/bin/pip
 ```
+
 !!! tip
     Every time you install a new package with `pip` under that environment, activate the environment again.
 
@@ -60,7 +61,7 @@ some/directory/fastapi/env/bin/pip
 
 **FastAPI** uses <a href="https://flit.readthedocs.io/en/latest/index.html" class="external-link" target="_blank">Flit</a> to build, package and publish the project.
 
-After activating the environment  as described above, install `flit`:
+After activating the environment as described above, install `flit`:
 
 ```console
 $ pip install flit
diff --git a/docs/css/termynal.css b/docs/css/termynal.css
new file mode 100644 (file)
index 0000000..0484e65
--- /dev/null
@@ -0,0 +1,108 @@
+/**
+ * termynal.js
+ *
+ * @author Ines Montani <ines@ines.io>
+ * @version 0.0.1
+ * @license MIT
+ */
+
+:root {
+    --color-bg: #252a33;
+    --color-text: #eee;
+    --color-text-subtle: #a2a2a2;
+}
+
+[data-termynal] {
+    width: 750px;
+    max-width: 100%;
+    background: var(--color-bg);
+    color: var(--color-text);
+    font-size: 18px;
+    /* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */
+    font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace;
+    border-radius: 4px;
+    padding: 75px 45px 35px;
+    position: relative;
+    -webkit-box-sizing: border-box;
+            box-sizing: border-box;
+}
+
+[data-termynal]:before {
+    content: '';
+    position: absolute;
+    top: 15px;
+    left: 15px;
+    display: inline-block;
+    width: 15px;
+    height: 15px;
+    border-radius: 50%;
+    /* A little hack to display the window buttons in one pseudo element. */
+    background: #d9515d;
+    -webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
+            box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
+}
+
+[data-termynal]:after {
+    content: 'bash';
+    position: absolute;
+    color: var(--color-text-subtle);
+    top: 5px;
+    left: 0;
+    width: 100%;
+    text-align: center;
+}
+
+a[data-terminal-control] {
+    text-align: right;
+    display: block;
+    color: #aebbff;
+}
+
+[data-ty] {
+    display: block;
+    line-height: 2;
+}
+
+[data-ty]:before {
+    /* Set up defaults and ensure empty lines are displayed. */
+    content: '';
+    display: inline-block;
+    vertical-align: middle;
+}
+
+[data-ty="input"]:before,
+[data-ty-prompt]:before {
+    margin-right: 0.75em;
+    color: var(--color-text-subtle);
+}
+
+[data-ty="input"]:before {
+    content: '$';
+}
+
+[data-ty][data-ty-prompt]:before {
+    content: attr(data-ty-prompt);
+}
+
+[data-ty-cursor]:after {
+    content: attr(data-ty-cursor);
+    font-family: monospace;
+    margin-left: 0.5em;
+    -webkit-animation: blink 1s infinite;
+            animation: blink 1s infinite;
+}
+
+
+/* Cursor animation */
+
+@-webkit-keyframes blink {
+    50% {
+        opacity: 0;
+    }
+}
+
+@keyframes blink {
+    50% {
+        opacity: 0;
+    }
+}
index 4973bc21569feee6612904cdfbfd62ed0c047a0d..8d4c6fcdba464fd2ad12ab367154e70cc4391ba6 100644 (file)
@@ -188,18 +188,28 @@ def read_item(item_id: int, q: str = None):
 * Go to the project directory (in where your `Dockerfile` is, containing your `app` directory).
 * Build your FastAPI image:
 
-```bash
-docker build -t myimage .
+<div class="termy">
+
+```console
+$ docker build -t myimage .
+
+---> 100%
 ```
 
+</div>
+
 ### Start the Docker container
 
 * Run a container based on your image:
 
-```bash
-docker run -d --name mycontainer -p 80:80 myimage
+<div class="termy">
+
+```console
+$ docker run -d --name mycontainer -p 80:80 myimage
 ```
 
+</div>
+
 Now you have an optimized FastAPI server in a Docker container. Auto-tuned for your current server (and number of CPU cores).
 
 ### Check it
@@ -319,30 +329,54 @@ You just need to install an ASGI compatible server like:
 
 * <a href="https://www.uvicorn.org/" class="external-link" target="_blank">Uvicorn</a>, a lightning-fast ASGI server, built on uvloop and httptools.
 
-```bash
-pip install uvicorn
+<div class="termy">
+
+```console
+$ pip install uvicorn
+
+---> 100%
 ```
 
+</div>
+
 * <a href="https://gitlab.com/pgjones/hypercorn" class="external-link" target="_blank">Hypercorn</a>, an ASGI server also compatible with HTTP/2.
 
-```bash
-pip install hypercorn
+<div class="termy">
+
+```console
+$ pip install hypercorn
+
+---> 100%
 ```
 
+</div>
+
 ...or any other ASGI server.
 
 And run your application the same way you have done in the tutorials, but without the `--reload` option, e.g.:
 
-```bash
-uvicorn main:app --host 0.0.0.0 --port 80
+<div class="termy">
+
+```console
+$ uvicorn main:app --host 0.0.0.0 --port 80
+
+<span style="color: green;">INFO</span>:     Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit)
 ```
 
+</div>
+
 or with Hypercorn:
 
-```bash
-hypercorn main:app --bind 0.0.0.0:80
+<div class="termy">
+
+```console
+$ hypercorn main:app --bind 0.0.0.0:80
+
+Running on 0.0.0.0:8080 over http (CTRL + C to quit)
 ```
 
+</div>
+
 You might want to set up some tooling to make sure it is restarted automatically if it stops.
 
 You might also want to install <a href="https://gunicorn.org/" class="external-link" target="_blank">Gunicorn</a> and <a href="https://www.uvicorn.org/#running-with-gunicorn" class="external-link" target="_blank">use it as a manager for Uvicorn</a>, or use Hypercorn with multiple workers.
index 8801c1af57b63965240088b49e4d155a9b588c26..11c9453720dc7b52eea625cd20063b4c93da0564 100644 (file)
@@ -77,6 +77,14 @@ The key features are:
 
 ---
 
+## **Typer**, the FastAPI of CLIs
+
+<a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a>
+
+If you are building a <abbr title="Command Line Interface">CLI</abbr> app to be used in the terminal instead of a web API, check out <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>.
+
+**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀
+
 ## Requirements
 
 Python 3.6+
@@ -88,16 +96,28 @@ FastAPI stands on the shoulders of giants:
 
 ## Installation
 
-```bash
-pip install fastapi
+<div class="termy">
+
+```console
+$ pip install fastapi
+
+---> 100%
 ```
 
+</div>
+
 You will also need an ASGI server, for production such as <a href="http://www.uvicorn.org" class="external-link" target="_blank">Uvicorn</a> or <a href="https://gitlab.com/pgjones/hypercorn" class="external-link" target="_blank">Hypercorn</a>.
 
-```bash
-pip install uvicorn
+<div class="termy">
+
+```console
+$ pip install uvicorn
+
+---> 100%
 ```
 
+</div>
+
 ## Example
 
 ### Create it
@@ -151,10 +171,20 @@ If you don't know, check the _"In a hurry?"_ section about <a href="https://fast
 
 Run the server with:
 
-```bash
-uvicorn main:app --reload
+<div class="termy">
+
+```console
+$ uvicorn main:app --reload
+
+<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
+<span style="color: green;">INFO</span>:     Started reloader process [28720]
+<span style="color: green;">INFO</span>:     Started server process [28722]
+<span style="color: green;">INFO</span>:     Waiting for application startup.
+<span style="color: green;">INFO</span>:     Application startup complete.
 ```
 
+</div>
+
 <details markdown="1">
 <summary>About the command <code>uvicorn main:app --reload</code>...</summary>
 
index 54bd53d2700b7043f91c443632bd20d3fc1bd08b..0f1d4852161671b9904526d511097c141e0c485e 100644 (file)
@@ -20,6 +20,114 @@ async function getData() {
     return data
 }
 
+function setupTermynal() {
+    document.querySelectorAll(".use-termynal").forEach(node => {
+        node.style.display = "block";
+        new Termynal(node, {
+            lineDelay: 500
+        });
+    });
+    const progressLiteralStart = "---> 100%";
+    const promptLiteralStart = "$ ";
+    const customPromptLiteralStart = "# ";
+    const termynalActivateClass = "termy";
+    let termynals = [];
+
+    function createTermynals() {
+        document
+            .querySelectorAll(`.${termynalActivateClass} .codehilite`)
+            .forEach(node => {
+                const text = node.textContent;
+                const lines = text.split("\n");
+                const useLines = [];
+                let buffer = [];
+                function saveBuffer() {
+                    if (buffer.length) {
+                        let isBlankSpace = true;
+                        buffer.forEach(line => {
+                            if (line) {
+                                isBlankSpace = false;
+                            }
+                        });
+                        dataValue = {};
+                        if (isBlankSpace) {
+                            dataValue["delay"] = 0;
+                        }
+                        if (buffer[buffer.length - 1] === "") {
+                            // A last single <br> won't have effect
+                            // so put an additional one
+                            buffer.push("");
+                        }
+                        const bufferValue = buffer.join("<br>");
+                        dataValue["value"] = bufferValue;
+                        useLines.push(dataValue);
+                        buffer = [];
+                    }
+                }
+                for (let line of lines) {
+                    if (line === progressLiteralStart) {
+                        saveBuffer();
+                        useLines.push({
+                            type: "progress"
+                        });
+                    } else if (line.startsWith(promptLiteralStart)) {
+                        saveBuffer();
+                        const value = line.replace(promptLiteralStart, "").trimEnd();
+                        useLines.push({
+                            type: "input",
+                            value: value
+                        });
+                    } else if (line.startsWith("// ")) {
+                        saveBuffer();
+                        const value = "💬 " + line.replace("// ", "").trimEnd();
+                        useLines.push({
+                            value: value,
+                            class: "termynal-comment",
+                            delay: 0
+                        });
+                    } else if (line.startsWith(customPromptLiteralStart)) {
+                        saveBuffer();
+                        const promptStart = line.indexOf(promptLiteralStart);
+                        if (promptStart === -1) {
+                            console.error("Custom prompt found but no end delimiter", line)
+                        }
+                        const prompt = line.slice(0, promptStart).replace(customPromptLiteralStart, "")
+                        let value = line.slice(promptStart + promptLiteralStart.length);
+                        useLines.push({
+                            type: "input",
+                            value: value,
+                            prompt: prompt
+                        });
+                    } else {
+                        buffer.push(line);
+                    }
+                }
+                saveBuffer();
+                const div = document.createElement("div");
+                node.replaceWith(div);
+                const termynal = new Termynal(div, {
+                    lineData: useLines,
+                    noInit: true,
+                    lineDelay: 500
+                });
+                termynals.push(termynal);
+            });
+    }
+
+    function loadVisibleTermynals() {
+        termynals = termynals.filter(termynal => {
+            if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) {
+                termynal.init();
+                return false;
+            }
+            return true;
+        });
+    }
+    window.addEventListener("scroll", loadVisibleTermynals);
+    createTermynals();
+    loadVisibleTermynals();
+}
+
 async function main() {
     if (div) {
         data = await getData()
@@ -34,6 +142,8 @@ async function main() {
             ul.append(li)
         })
     }
+
+    setupTermynal();
 }
 
 main()
diff --git a/docs/js/termynal.js b/docs/js/termynal.js
new file mode 100644 (file)
index 0000000..8b0e933
--- /dev/null
@@ -0,0 +1,264 @@
+/**
+ * termynal.js
+ * A lightweight, modern and extensible animated terminal window, using
+ * async/await.
+ *
+ * @author Ines Montani <ines@ines.io>
+ * @version 0.0.1
+ * @license MIT
+ */
+
+'use strict';
+
+/** Generate a terminal widget. */
+class Termynal {
+    /**
+     * Construct the widget's settings.
+     * @param {(string|Node)=} container - Query selector or container element.
+     * @param {Object=} options - Custom settings.
+     * @param {string} options.prefix - Prefix to use for data attributes.
+     * @param {number} options.startDelay - Delay before animation, in ms.
+     * @param {number} options.typeDelay - Delay between each typed character, in ms.
+     * @param {number} options.lineDelay - Delay between each line, in ms.
+     * @param {number} options.progressLength - Number of characters displayed as progress bar.
+     * @param {string} options.progressChar – Character to use for progress bar, defaults to █.
+        * @param {number} options.progressPercent - Max percent of progress.
+     * @param {string} options.cursor – Character to use for cursor, defaults to ▋.
+     * @param {Object[]} lineData - Dynamically loaded line data objects.
+     * @param {boolean} options.noInit - Don't initialise the animation.
+     */
+    constructor(container = '#termynal', options = {}) {
+        this.container = (typeof container === 'string') ? document.querySelector(container) : container;
+        this.pfx = `data-${options.prefix || 'ty'}`;
+        this.originalStartDelay = this.startDelay = options.startDelay
+            || parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600;
+        this.originalTypeDelay = this.typeDelay = options.typeDelay
+            || parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90;
+        this.originalLineDelay = this.lineDelay = options.lineDelay
+            || parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500;
+        this.progressLength = options.progressLength
+            || parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40;
+        this.progressChar = options.progressChar
+            || this.container.getAttribute(`${this.pfx}-progressChar`) || '█';
+               this.progressPercent = options.progressPercent
+            || parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100;
+        this.cursor = options.cursor
+            || this.container.getAttribute(`${this.pfx}-cursor`) || '▋';
+        this.lineData = this.lineDataToElements(options.lineData || []);
+        this.loadLines()
+        if (!options.noInit) this.init()
+    }
+
+    loadLines() {
+        // Load all the lines and create the container so that the size is fixed
+        // Otherwise it would be changing and the user viewport would be constantly
+        // moving as she/he scrolls
+        const finish = this.generateFinish()
+        finish.style.visibility = 'hidden'
+        this.container.appendChild(finish)
+        // Appends dynamically loaded lines to existing line elements.
+        this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData);
+        for (let line of this.lines) {
+            line.style.visibility = 'hidden'
+            this.container.appendChild(line)
+        }
+        const restart = this.generateRestart()
+        restart.style.visibility = 'hidden'
+        this.container.appendChild(restart)
+        this.container.setAttribute('data-termynal', '');
+    }
+
+    /**
+     * Initialise the widget, get lines, clear container and start animation.
+     */
+    init() {
+        /** 
+         * Calculates width and height of Termynal container.
+         * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS.
+         */ 
+        const containerStyle = getComputedStyle(this.container);
+        this.container.style.width = containerStyle.width !== '0px' ? 
+            containerStyle.width : undefined;
+        this.container.style.minHeight = containerStyle.height !== '0px' ? 
+            containerStyle.height : undefined;
+
+        this.container.setAttribute('data-termynal', '');
+        this.container.innerHTML = '';
+        for (let line of this.lines) {
+            line.style.visibility = 'visible'
+        }
+        this.start();
+    }
+
+    /**
+     * Start the animation and rener the lines depending on their data attributes.
+     */
+    async start() {
+        this.addFinish()
+        await this._wait(this.startDelay);
+
+        for (let line of this.lines) {
+            const type = line.getAttribute(this.pfx);
+            const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay;
+
+            if (type == 'input') {
+                line.setAttribute(`${this.pfx}-cursor`, this.cursor);
+                await this.type(line);
+                await this._wait(delay);
+            }
+
+            else if (type == 'progress') {
+                await this.progress(line);
+                await this._wait(delay);
+            }
+
+            else {
+                this.container.appendChild(line);
+                await this._wait(delay);
+            }
+
+            line.removeAttribute(`${this.pfx}-cursor`);
+        }
+        this.addRestart()
+        this.finishElement.style.visibility = 'hidden'
+        this.lineDelay = this.originalLineDelay
+        this.typeDelay = this.originalTypeDelay
+        this.startDelay = this.originalStartDelay
+    }
+
+    generateRestart() {
+        const restart = document.createElement('a')
+        restart.onclick = (e) => {
+            e.preventDefault()
+            this.container.innerHTML = ''
+            this.init()
+        }
+        restart.href = '#'
+        restart.setAttribute('data-terminal-control', '')
+        restart.innerHTML = "restart ↻"
+        return restart
+    }
+    
+    generateFinish() {
+        const finish = document.createElement('a')
+        finish.onclick = (e) => {
+            e.preventDefault()
+            this.lineDelay = 0
+            this.typeDelay = 0
+            this.startDelay = 0
+        }
+        finish.href = '#'
+        finish.setAttribute('data-terminal-control', '')
+        finish.innerHTML = "fast →"
+        this.finishElement = finish
+        return finish
+    }
+
+    addRestart() {
+        const restart = this.generateRestart()
+        this.container.appendChild(restart)
+    }
+
+    addFinish() {
+        const finish = this.generateFinish()
+        this.container.appendChild(finish)
+    }
+
+    /**
+     * Animate a typed line.
+     * @param {Node} line - The line element to render.
+     */
+    async type(line) {
+        const chars = [...line.textContent];
+        line.textContent = '';
+        this.container.appendChild(line);
+
+        for (let char of chars) {
+            const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay;
+            await this._wait(delay);
+            line.textContent += char;
+        }
+    }
+
+    /**
+     * Animate a progress bar.
+     * @param {Node} line - The line element to render.
+     */
+    async progress(line) {
+        const progressLength = line.getAttribute(`${this.pfx}-progressLength`)
+            || this.progressLength;
+        const progressChar = line.getAttribute(`${this.pfx}-progressChar`)
+            || this.progressChar;
+        const chars = progressChar.repeat(progressLength);
+               const progressPercent = line.getAttribute(`${this.pfx}-progressPercent`)
+                       || this.progressPercent;
+        line.textContent = '';
+        this.container.appendChild(line);
+
+        for (let i = 1; i < chars.length + 1; i++) {
+            await this._wait(this.typeDelay);
+            const percent = Math.round(i / chars.length * 100);
+            line.textContent = `${chars.slice(0, i)} ${percent}%`;
+                       if (percent>progressPercent) {
+                               break;
+                       }
+        }
+    }
+
+    /**
+     * Helper function for animation delays, called with `await`.
+     * @param {number} time - Timeout, in ms.
+     */
+    _wait(time) {
+        return new Promise(resolve => setTimeout(resolve, time));
+    }
+
+    /**
+     * Converts line data objects into line elements.
+     * 
+     * @param {Object[]} lineData - Dynamically loaded lines.
+     * @param {Object} line - Line data object.
+     * @returns {Element[]} - Array of line elements.
+     */
+    lineDataToElements(lineData) {
+        return lineData.map(line => {
+            let div = document.createElement('div');
+            div.innerHTML = `<span ${this._attributes(line)}>${line.value || ''}</span>`;
+
+            return div.firstElementChild;
+        });
+    }
+
+    /**
+     * Helper function for generating attributes string.
+     * 
+     * @param {Object} line - Line data object.
+     * @returns {string} - String of attributes.
+     */
+    _attributes(line) {
+        let attrs = '';
+        for (let prop in line) {
+            // Custom add class
+            if (prop === 'class') {
+                attrs += ` class=${line[prop]} `
+                continue
+            }
+            if (prop === 'type') {
+                attrs += `${this.pfx}="${line[prop]}" `
+            } else if (prop !== 'value') {
+                attrs += `${this.pfx}-${prop}="${line[prop]}" `
+            }
+        }
+
+        return attrs;
+    }
+}
+
+/**
+* HTML API: If current script has container(s) specified, initialise Termynal.
+*/
+if (document.currentScript.hasAttribute('data-termynal-container')) {
+    const containers = document.currentScript.getAttribute('data-termynal-container');
+    containers.split('|')
+        .forEach(container => new Termynal(container))
+}
index 6ee298337fe4f855b49dba40ae205fda9ce2eb23..f6c623b840b3ab8ab4be05fcb4a7da3e8cf8db70 100644 (file)
@@ -292,10 +292,16 @@ The end result is that the item paths are now:
 
 Now, run `uvicorn`, using the module `app.main` and the variable `app`:
 
-```bash
-uvicorn app.main:app --reload
+<div class="termy">
+
+```console
+$ uvicorn app.main:app --reload
+
+<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
 ```
 
+</div>
+
 And open the docs at <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>.
 
 You will see the automatic API docs, including the paths from all the submodules, using the correct paths (and prefixes) and the correct tags:
index 3800abd1b70ee59ac92979e90eb07103b3399da3..e527bf9712363ec0fc43a9f9a1770505e223930b 100644 (file)
@@ -12,10 +12,14 @@ In your FastAPI application, import and run `uvicorn` directly:
 
 The main purpose of the `__name__ == "__main__"` is to have some code that is executed when your file is called with:
 
-```bash
-python myapp.py
+<div class="termy">
+
+```console
+$ python myapp.py
 ```
 
+</div>
+
 but is not called when another file imports it, like in:
 
 ```Python
@@ -28,10 +32,14 @@ Let's say your file is named `myapp.py`.
 
 If you run it with:
 
-```bash
-python myapp.py
+<div class="termy">
+
+```console
+$ python myapp.py
 ```
 
+</div>
+
 then the internal variable `__name__` in your file, created automatically by Python, will have as value the string `"__main__"`.
 
 So, the section:
index dd916f3304a800b137b24823cce6ebbb4be5d317..639cfed395658185c5954244d78f524bfc8c0f68 100644 (file)
@@ -8,10 +8,20 @@ Copy that to a file `main.py`.
 
 Run the live server:
 
-```bash
-uvicorn main:app --reload
+<div class="termy">
+
+```console
+$ uvicorn main:app --reload
+
+<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
+<span style="color: green;">INFO</span>:     Started reloader process [28720]
+<span style="color: green;">INFO</span>:     Started server process [28722]
+<span style="color: green;">INFO</span>:     Waiting for application startup.
+<span style="color: green;">INFO</span>:     Application startup complete.
 ```
 
+</div>
+
 !!! note
     The command `uvicorn main:app` refers to:
 
@@ -19,16 +29,13 @@ uvicorn main:app --reload
     * `app`: the object created inside of `main.py` with the line `app = FastAPI()`.
     * `--reload`: make the server restart after code changes. Only use for development.
 
-You will see an output like:
+In the output, there's a line with something like:
 
 ```hl_lines="4"
-INFO: Started reloader process [17961]
-INFO: Started server process [17962]
-INFO: Waiting for application startup.
-INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
+INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
 ```
 
-That last line shows the URL where your app is being served, in your local machine.
+That line shows the URL where your app is being served, in your local machine.
 
 ### Check it
 
@@ -144,10 +151,16 @@ This will be the main point of interaction to create all your API.
 
 This `app` is the same one referred by `uvicorn` in the command:
 
-```bash
-uvicorn main:app --reload
+<div class="termy">
+
+```console
+$ uvicorn main:app --reload
+
+<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
 ```
 
+</div>
+
 If you create your app like:
 
 ```Python hl_lines="3"
@@ -156,10 +169,16 @@ If you create your app like:
 
 And put it in a file `main.py`, then you would call `uvicorn` like:
 
-```bash
-uvicorn main:my_awesome_api --reload
+<div class="termy">
+
+```console
+$ uvicorn main:my_awesome_api --reload
+
+<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
 ```
 
+</div>
+
 ### Step 3: create a *path operation*
 
 #### Path
index 0aa572c88e9d4d7c93950fb347dc8a4bf5ec4957..ae073518ae4861a4232d1298258d498c2a9b35d0 100644 (file)
@@ -12,10 +12,20 @@ All the code blocks can be copied and used directly (they are actually tested Py
 
 To run any of the examples, copy the code to a file `main.py`, and start `uvicorn` with:
 
-```bash
-uvicorn main:app --reload
+<div class="termy">
+
+```console
+$ uvicorn main:app --reload
+
+<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
+<span style="color: green;">INFO</span>:     Started reloader process [28720]
+<span style="color: green;">INFO</span>:     Started server process [28722]
+<span style="color: green;">INFO</span>:     Waiting for application startup.
+<span style="color: green;">INFO</span>:     Application startup complete.
 ```
 
+</div>
+
 It is **HIGHLY encouraged** that you write or copy the code, edit it and run it locally.
 
 Using it in your editor is what really shows you the benefits of FastAPI, seeing how little code you have to write, all the type checks, autocompletion, etc.
@@ -28,10 +38,16 @@ The first step is to install FastAPI.
 
 For the tutorial, you might want to install it with all the optional dependencies and features:
 
-```bash
-pip install fastapi[all]
+<div class="termy">
+
+```console
+$ pip install fastapi[all]
+
+---> 100%
 ```
 
+</div>
+
 ...that also includes `uvicorn`, that you can use as the server that runs your code.
 
 !!! note
index 5d215bdd0540ad994053f822a3fc7fe60adfda2c..6fef16235585cbe0341c884b2dd7c9ec9e4d6ac9 100644 (file)
@@ -33,10 +33,16 @@ Copy the example in a file `main.py`:
 
 Run the example with:
 
-```bash
-uvicorn main:app --reload
+<div class="termy">
+
+```console
+$ uvicorn main:app --reload
+
+<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
 ```
 
+</div>
+
 ## Check it
 
 Go to the interactive docs at: <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>.
index 70945f2368bb3e13e0ba8c1ca637edb703bc5ed7..cd15cb9d293fc05a059856fb5f3f61881a096edb 100644 (file)
@@ -28,10 +28,16 @@ If you want to play with JWT tokens and see how they work, check <a href="https:
 
 We need to install `PyJWT` to generate and verify the JWT tokens in Python:
 
-```bash
-pip install pyjwt
+<div class="termy">
+
+```console
+$ pip install pyjwt
+
+---> 100%
 ```
 
+</div>
+
 ## Password hashing
 
 "Hashing" means converting some content (a password in this case) into a sequence of bytes (just a string) that looks like gibberish.
@@ -56,10 +62,16 @@ The recommended algorithm is "Bcrypt".
 
 So, install PassLib with Bcrypt:
 
-```bash
-pip install passlib[bcrypt]
+<div class="termy">
+
+```console
+$ pip install passlib[bcrypt]
+
+---> 100%
 ```
 
+</div>
+
 !!! tip
     With `passlib`, you could even configure it to be able to read passwords created by **Django**, a **Flask** security plug-in or many others.
 
@@ -101,10 +113,16 @@ Create a random secret key that will be used to sign the JWT tokens.
 
 To generate a secure random secret key use the command:
 
-```bash
-openssl rand -hex 32
+<div class="termy">
+
+```console
+$ openssl rand -hex 32
+
+09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7
 ```
 
+</div>
+
 And copy the output to the variable `SECRET_KEY` (don't use the one in the example).
 
 Create a variable `ALGORITHM` with the algorithm used to sign the JWT token and set it to `"HS256"`.
index 77fd2c8f46ab5d244d741bd540ff73840d933cd9..ba4b836483140d7997a3221a7a69a7783336241f 100644 (file)
@@ -440,8 +440,8 @@ A "migration" is the set of steps needed whenever you change the structure of yo
 !!! info
     For this to work, you need to use **Python 3.7** or above, or in **Python 3.6**, install the "backports":
 
-    ```bash
-    pip install async-exit-stack async-generator
+    ```console
+    pip install async-exit-stack async-generator
     ```
 
     This installs <a href="https://github.com/sorcio/async_exit_stack" class="external-link" target="_blank">async-exit-stack</a> and <a href="https://github.com/python-trio/async_generator" class="external-link" target="_blank">async-generator</a>.
@@ -596,10 +596,17 @@ You can copy this code and use it as is.
 
 Then you can run it with Uvicorn:
 
-```bash
-uvicorn sql_app.main:app --reload
+
+<div class="termy">
+
+```console
+$ uvicorn sql_app.main:app --reload
+
+<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
 ```
 
+</div>
+
 And then, you can open your browser at <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>.
 
 And you will be able to interact with your **FastAPI** application, reading data from a real database:
index 1b0725291311d16b89cb72cc1ec7f948da66f6fc..0a1d639565344c207ea1db43b60ed14e3b8d0c88 100644 (file)
@@ -4,10 +4,16 @@ You can serve static files automatically from a directory using `StaticFiles`.
 
 First you need to install `aiofiles`:
 
-```bash
-pip install aiofiles
+<div class="termy">
+
+```console
+$ pip install aiofiles
+
+---> 100%
 ```
 
+</div>
+
 ## Use `StaticFiles`
 
 * Import `StaticFiles`.
index 1b1da2ab28a9f8d8f72cb477b6726ca091423292..6733527728c94e87f71088076a4f49dbbf33075e 100644 (file)
@@ -103,14 +103,36 @@ For more information about how to pass data to the backend (using `requests` or
 
 After that, you just need to install `pytest`:
 
-```bash
-pip install pytest
+<div class="termy">
+
+```console
+$ pip install pytest
+
+---> 100%
 ```
 
+</div>
+
 It will detect the files and tests automatically, execute them, and report the results back to you.
 
 Run the tests with:
 
-```bash
-pytest
+<div class="termy">
+
+```console
+$ pytest
+
+================ test session starts ================
+platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
+rootdir: /home/user/code/superawesome-cli/app
+plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1
+collected 6 items
+
+---> 100%
+
+test_main.py <span style="color: green; white-space: pre;">......                            [100%]</span>
+
+<span style="color: green;">================= 1 passed in 0.03s =================</span>
 ```
+
+</div>
index b1f88184f10ee8c7ec84fbf5930b1a854d0eafac..bacae9baca88670e51f2b57b5bea395923425452 100644 (file)
@@ -141,8 +141,10 @@ extra:
           link: 'https://tiangolo.com'
 
 extra_css:
+    - 'css/termynal.css'
     - 'css/custom.css'
 
 extra_javascript:
     - 'https://unpkg.com/mermaid@8.4.6/dist/mermaid.min.js'
+    - 'js/termynal.js'
     - 'js/custom.js'