Announcing the first Flask Conference in Brazil - August 2018

Hello,

The first Flask conference will be held in São Paulo, Brazil, on August 24 and 25, 2018.

It will be a day of tutorial and a day of lectures for over 160 people.

At this moment the organization is in search of sponsorship to enable the coming of one of the core-developers to be Keynote of this event.

According to StackShare [1] more than 1.6k of companies are using Flask, large companies like Uber, Netflix, Cloudify, Lift, Red Hat and many others rely on Flask to serve web applications and APIs.

We believe it is time for Flask to have its annual conference and we are starting in São Paulo Brazil and we hope this idea will spread to other places in the world!

Please help!

We need sponsorship: Help us by sharing the link below with companies that can support the event:

https://2018.flask.python.org.br/FlaskConf2018-sponsorship-en.pdf

Submit lectures (or vote in the lectures) - * The event is in Brazil but participants from anywhere in the world are warmly welcomed! And lectures in English will also be interesting!

https://speakerfight.com/events/flaskconf-2018/

Come to Flask Conf 2018!

https://www.eventbrite.com/e/flask-conf-registration-46990838864

Flask Conf 2018 https://2018.flask.python.org.br/

[1] https://stackshare.io/flask/in-stacks/

Dynaconf 1.0.x released - Layered configuration system for python with flask and django support

Dynaconf

dynaconf - The dynamic configurator for your Python Project

MIT License PyPI PyPI Travis CI codecov Codacy grade

dynaconf a layered configuration system for Python applications - with strong support for 12-factor applications and extensions for Flask and Django.

Features

  • Strict separation of settings from code (following 12-factor applications Guide).
  • Define comprehensive default values.
  • Store parameters in multiple file formats (.toml, .json, .yaml, .ini and .py).
  • Sensitive secrets like tokens and passwords can be stored in safe places like .secrets file or vault server.
  • Parameters can optionally be stored in external services like Redis server.
  • Simple feature flag system.
  • Layered [environment] system.
  • Environment variables can be used to override parameters.
  • Support for .env files to automate the export of environment variables.
  • Correct data types (even for environment variables).
  • Have only one canonical settings module to rule all your instances.
  • Drop in extension for Flask app.config object.
  • Drop in extension for Django conf.settings object.
  • Powerful $ dynaconf CLI to help you manage your settings via console.
  • Customizable Validation System to ensure correct config parameters.
  • Allow the change of dynamic parameters on the fly without the need to redeploy your application.
  • Easily extensible
  • 100% test coverage
  • 100% documented

Read the Full Documentation at: http://dynaconf.readthedocs.io/

Repository: http://github.com/rochacbruno/dynaconf/

Getting Started

Installation

Python 3.x is required

$ pip install dynaconf

Default installation supports .toml, .py and .json file formats and also environment variables (.env supported)

Usage

Accessing config variables in your Python application

In your Python program wherever you need to access a settings variable you use the canonical object from dynaconf import settings:

NOTE: Read the full documentation for more examples like using Dynaconf with Flask or Django

Example of program to connect to some database

from some.db import Client

from dynaconf import settings               # The only object you need to import

conn = Client(
    username=settings.USERNAME,             # attribute style access
    password=settings.get('PASSWORD'),      # dict get style access
    port=settings['PORT'],                  # dict item style access
    timeout=settings.as_int('TIMEOUT'),     # Forcing casting if needed
    host=settings.get('HOST', 'localhost')  # Providing defaults
)

Where the settings values are stored

Dynaconf aims to have a flexible and usable configuration system. Your applications can be configured via a configuration files, through environment variables, or both. Configurations are separated into environments: [development], [staging], [testing] and [production]. The working environment is selected via an environment variable.

Sensitive data like tokens, secret keys and password can be stored in .secrets.* files and/or external storages like Redis or vault secrets server.

Besides the built-in optional support to redis as settings storage dynaconf allows you to create custom loaders and store the data wherever you want e.g: databases, memory storages, other file formats, nosql databases etc.

Working environments

At any point in time, your application is operating in a given configuration environment. By default there are four such environments:

  • [development]
  • [staging]
  • [testing]
  • [production]

You can also define [custom environment] and use the pseudo-envs [default] to provide comprehensive default values and [global] to provide global values to overrride in any other environment.

Without any action, your applications by default run in the [development] environment. The environment can be changed via the ÈNV_FOR_DYNACONF environment variable. For example, to launch an application in the [staging] environment, we can run:

export ENV_FOR_DYNACONF=staging

or

ENV_FOR_DYNACONF=staging python yourapp.py

NOTE: When using FLask Extension the environment can be changed via FLASK_ENV variable and for Django Extension you can use DJANGO_ENV.

The settings files

NOTE: Read the full documentaion about dynaconf CLI to learn how to automatically create the settings files for your project.

An optional settings.{toml|py|json|ini|yaml} file can be used to specify the configuration parameters for each environment. If it is not present, only the values from environment variables are used (.env file is also supported). Dynaconf searches for the file starting at the current working directory. If it is not found there, Dynaconf checks the parent directory. Dynaconf continues checking parent directories until the root is reached.

The recommended file format is TOML but you can choose to use any of .{toml|py|json|ini|yaml}.

The file must be a series of sections, at most one for [default], optionally one for each [environment], and an optional [global] section. Each section contains key-value pairs corresponding to configuration parameters for that [environment]. If a configuration parameter is missing, the value from [default] is used. The following is a complete settings.toml file, where every standard configuration parameter is specified within the [default] section:

NOTE: if the file format choosen is .py as it does not support sections you can create multiple files like settings.py for [default], development_settings.py, production_settings.py and global_settings.py. ATTENTION using .py is not recommended for configuration use TOML!

[default]
username = "admin"
port = 5000
host = "localhost"
message = "default message"
value = "default value"

[development]
username = "devuser"

[staging]
host = "staging.server.com"

[testing]
host = "testing.server.com"

[production]
host = "server.com"

[awesomeenv]
value = "this value is set for custom [awesomeenv]"

[global]
message = "This value overrides message of default and other envs"

The [global] pseudo-environment can be used to set and/or override configuration parameters globally. A parameter defined in a [global] section sets, or overrides if already present, that parameter in every environment. For example, given the following settings.toml file, the value of address will be "1.2.3.4" in every environment:

[global]
address = "1.2.3.4"

[development]
address = "localhost"

[production]
address = "0.0.0.0"

NOTE: The [env] name and first level variables are case insensitive as internally dynaconf will always use upper case, that means [development] and [DEVELOPMENT] are equivalent and address and ADDRESS are also equivalent. This rule does not apply for inner data structures as dictionaries and arrays.

Supported file formats

By default toml is the recommended format to store your configuration, however you can switch to a different supported format.

# If you wish to include support for more sources
pip3 install dynaconf[yaml|ini|redis|vault]

# for a complete installation
pip3 install dynaconf[all]

Once the support is installed no extra configuration is needed to load data from those files, dynaconf will search for settings files in the root directory of your application looking for the following files in the exact order below:

DYNACONF_LOADING_ORDER = [
 'settings.py',
 '.secrets.py',
 'settings.toml',
 '.secrets.toml',
 'settings.yaml',
 '.secrets.yaml',
 'settings.ini',
 '.secrets.ini',
 'settings.json',
 '.secrets.json',
 # redis server if REDIS_ENABLED_FOR_DYNACONF=true
 # vault server if VAULT_ENABLED_FOR_DYNACONF=true
 # other sources if custom loaders are defined
 # All environment variables prefixed with DYNACONF_
]

NOTE: Dynaconf works in an layered override mode based on the above order, so if you have multiple file formats with conflicting keys defined, the precedence will be based on the loading order.

Take a look at the example folder to see some examples of use with different file formats.

Sensitive secrets

Using .secrets files

To safely store sensitive data Dynaconf also searches for a .secrets.{toml|py|json|ini|yaml} file to look for data like tokens and passwords.

example .secrets.toml:

[default]
password = "sek@987342$"

The secrets file supports all the environment definitions supported in the settings file.

IMPORTANT: The reason to use a .secrets.* file is the ability to omit this file when commiting to the repository so a recommended .gitignore should include .secrets.* line.

Using Vault server

The vaultproject.io/ is a key:value store for secrets and Dynaconf can load variables from a Vault secret.

  1. Run a vault server

Run a Vault server installed or via docker:

$ docker run -d -e 'VAULT_DEV_ROOT_TOKEN_ID=myroot' -p 8200:8200 vault
  1. Install support for vault in dynaconf
$ pip install dynaconf[vault]
  1. In your .env file or in exported environment variables define:
VAULT_ENABLED_FOR_DYNACONF=true
VAULT_URL_FOR_DYNACONF="http://localhost:8200"
VAULT_TOKEN_FOR_DYNACONF="myroot"

Now you can have keys like PASSWORD and TOKEN defined in the vault and dynaconf will read it.

To write a new secret you can use http://localhost:8200 web admin and write keys under the /secret/dynaconf secret database.

You can also use the Dynaconf writer via console

$ dynaconf write vault -s password=123456

Environment variables

overloading parameters via env vars

All configuration parameters, including custom environments and dynaconf configuration, can be overridden through environment variables.

To override the configuration parameter {param}, use an environment variable named DYNACONF_{PARAM}. For instance, to override the "HOST" configuration parameter, you can run your application with:

DYNACONF_HOST='otherhost.com' python yourapp.py

.env files

If you don't want to declare the variables on every program call you can run export DYNACONF_{PARAM} in your shell or put the values in a .env file located in the same directory as your settings files (the root directory of your application), variables in .env does not overrride existing environment variables.

IMPORTANT: Dynaconf will search for a .env located in the root directory of your application, if not found it will continue searching in parent directories until it reaches the root. To avoid conflicts we recommend to have a .env even if it is empty.

Precedence and type casting

Environment variables take precedence over all other configuration sources: if the variable is set, it will be used as the value for the parameter even if parameter exists in settings files or in .env.

Variable values are parsed as if they were TOML syntax. As illustration, consider the following examples:

# Numbers
DYNACONF_INTEGER=42
DYNACONF_FLOAT=3.14

# Text
DYNACONF_STRING=Hello
DYNACONF_STRING="Hello"

# Booleans
DYNACONF_BOOL=true
DYNACONF_BOOL=false

# Use extra quotes to force a string from other type
DYNACONF_STRING="'42'"
DYNACONF_STRING="'true'"

# Arrays must be homogenous in toml syntax
DYNACONF_ARRAY=[1, 2, 3]
DYNACONF_ARRAY=[1.1, 2.2, 3.3]
DYNACONF_ARRAY=['a', 'b', 'c']

# Dictionaries
DYNACONF_DICT={key="abc",val=123}

# toml syntax does not allow `None/null` values so use @none
DYNACONF_NONE='@none None'

# toml syntax does not allow mixed type arrays so use @json
DYNACONF_ARRAY='@json [42, 3.14, "hello", true, ["otherarray"], {"foo": "bar"}]'

NOTE: Older versions of Dynaconf used the @casting prefixes for env vars like export DYNACONF_INTEGER='@int 123' still works but this casting is deprecated in favor of using TOML syntax described above. To disable the @casting do export AUTO_CAST_FOR_DYNACONF=false

The global prefix

The DYNACONF_{param} prefix is set by GLOBAL_ENV_FOR_DYNACONF and serves only to be used in environment variables to override config values.

This prefix itself can be changed to something more significant for your application, however we recommend kepping DYNACONF_{param} as your global env prefix.

NOTE: See the Configuring dynaconf section in documentation to learn more on how to use .env variables to configure dynaconf behavior.

Flask Extension

Dynaconf provides a drop in replacement for app.config.

As Flask encourages the composition by overriding the config_class attribute this extension follows the patterns of Flask and turns your Flask's app.config in to a dynaconf instance.

Initialize the extension

Initialize the FlaskDynaconf extension in your app

from flask import Flask
from dynaconf import FlaskDynaconf

app = Flask(__name__)
FlaskDynaconf(app)

You can optionally use init_app as well.

Use FLASK_ environment variables

Then the app.config will work as a dynaconf.settings instance and FLASK_ will be the global prefix for exporting environment variables.

Example:

export FLASK_DEBUG=true              # app.config.DEBUG
export FLASK_INTVALUE=1              # app.config['INTVALUE']
export FLASK_MAIL_SERVER='host.com'  # app.config.get('MAIL_SERVER')

Settings files

You can also have settings files for your Flask app, in the root directory (the same where you execute flask run) put your settings.toml and .secrets.toml files and then define your environments [default], [development] and [production].

To switch the working environment the FLASK_ENV variable can be used, so FLASK_ENV=development to work in development mode or FLASK_ENV=production to switch to production.

IMPORTANT: To use $ dynaconf CLI the FLASK_APP must be defined.

IF you don't want to manually create your config files take a look at the CLI

Django Extension

Dynaconf a drop in replacement to django.conf.settings.

Following this pattern recommended pattern this extension makes your Django's conf.settings in to a dynaconf instance.

Initialize the extension

In your django project's settings.py include:

INSTALLED_APPS = [
    'dynaconf.contrib.django_dynaconf',
    ...
]

NOTE: The extension must be included as the first INSTALLED_APP of the list

Use DJANGO_ environment variables

Then django.conf.settings will work as a dynaconf.settings instance and DJANGO_ will be the global prefix to export environment variables.

Example:

export DJANGO_DEBUG=true     # django.conf.settings.DEBUG
export DJANGO_INTVALUE=1     # django.conf.settings['INTVALUE]
export DJANGO_HELLO="Hello"  # django.conf.settings.get('HELLO)

Settings files

You can also have settings files for your Django app, in the root directory (the same where manage.py is located) put your settings.toml and .secrets.toml files and then define your environments [default], [development] and [production].

To switch the working environment the DJANGO_ENV variable can be used, so DJANGO_ENV=development to work in development mode or DJANGO_ENV=production to switch to production.

IMPORTANT: To use $ dynaconf CLI the DJANGO_SETTINGS_MODULE must be defined.

IF you don't want to manually create your config files take a look at the CLI

NOTE: It is recommended that all the django's internal config vars should be kept in the settings.py of your project, then application specific values you can place in dynaconf's settings.toml in the root (same folder as manage.py). You can override settings.py values in the dynaconf settings file as well.

How to contribute

In github repository issues and Pull Request are welcomed!

  • New implementations
  • Bug Fixes
  • Bug reports
  • More examples of use in /example folder
  • Documentation
  • Feedback as issues and comments ot joining dynaconf on Telegram or #dynaconf on freenode
  • Donation to rochacbruno [at] gmail.com in PayPal

Read the docs

Documentation: http://dynaconf.readthedocs.io/

Repository: http://github.com/rochacbruno/dynaconf/

py2rs - from Python to Rust - Reference Guide

py2rs

From Python into Rust

let x = Rust::from("Python");

A quick reference guide for the Pythonista in process of becoming a Rustacean.

NOTE The original repository for this article is on https://github.com/rochacbruno/py2rs <-- READ THERE FOR UPDATED VERSION

Monty Python - Season 3 - Episode 49

The sketch

Mrs. Jalin: George.
Mr. Jalin: Yes, Gladys.
Mrs. Jalin: There's a man at the door with a moustache.
Mr. Jalin: Tell him I've already got one. (Mrs. Jalin hits him hard with a newspaper) 
          All right, all right. What's he want then?
Mrs. Jalin: He says do we want a documentary on crustaceans.
Mr. Jalin: Crustaceans!
Mrs. Jalin: Yes.
Mr. Jalin: What's he mean, crustaceans?
Mrs. Jalin: CRUSTACEANS!! GASTROPODS! LAMELLIBRANCHS! CEPHALOPODS!
...
Ok...watch it later... let's learn some Rust now...

TOC

Getting Started with Rust

Assuming you already know what is Rust and already decided to start learning it. Here are some steps for you to follow:

  1. Take a tour of Rust Syntax and Coding Style
    https://learnxinyminutes.com/docs/rust/
  2. Watch some screencasts to get basics of Ownership &Borrowing concept
    http://intorust.com/
  3. Follow this set of runnable examples to understand how everything fit together
    https://rustbyexample.com
  4. Now it is time to read your first book, you can pick:

  5. Read some real examples

  6. Patterns and Good Practices

Exercices

Time to put your new knowledge in action solving some exercices.

1) Exercism.io
Register a new account on exercism.io (using github auth)
Install exercism command line client on your computer
Solve some exercices: http://www.exercism.io/languages/rust/about

2) Rust Playground
Run Live Rust Code in the browser with https://play.rust-lang.org/

Getting updated

Now I assume you are addicted to Rust and you want to be updated about averything around it, here are some good links to follow.

1) This Week in Rust Newsletter
https://this-week-in-rust.org/
https://twitter.com/thisweekinrust 2) Reddit
http://reddit.com/r/rust (serious sub-reddit)
http://reddit.com/r/rustjerk (almost memes only)
3) Official Twitter
https://twitter.com/rustlang

Interact with other Rustaceans

Don't be afraid, the Rustaceans are a very receptive species and are cozy with the Pythonistas.

Community links: https://www.rust-lang.org/en-US/community.html

Local

Additional learning resources

Facts

More facts? send a question here or send a Pull Request adding an interest fact to this list.

ferris

Glossary of terms

Term Definition
crate A rust distributable package
ferris The unofficial Crab Mascot
Rustacean The Rust programmer or evangelist or enthusiastic
nightly The unstable toolchain of the Rust compiler
impl Implementation

py2rs

From Python into Rust

let x = Rust::from("Python");

A quick reference guide for the Pythonista in process of becoming a Rustacean.

General

Python Definition Rust
PEP8 Guidelines and conventions RustAPI Guidelines
PEPS Enhancement Proposals / RFC Rust RFCs
PSF Organization / Foundation Mozilla Research
PyCon Main Conference RustConf
Guido Van Rossum Creator Graydon Hoare
1989 First appeared 2010
1991 First Release 2012
PSF License Apache 2.0 and MIT
C Implemented in Rust
.py, .pyw, .pyc File Extensions .rs, .rlib
http://github.com/python/cpython Repository https://github.com/rust-lang/rust
Pyladies, AfroPython Diversity and Inclusion initiative RustBridge
comp.lang.Python Official Users Forum users.rust-lang.org

Environment Tools

Python Definition Rust
requirements.txt Official dependency tracker file Cargo.toml
setup.py Official installator / distributor file Cargo.toml
PyPI Library Repositoty Crates.io
pip Library installation Cargo
setuptools Library distribution Cargo
pbr Library distribution Cargo
pipenv Dependency manager Cargo
twine Package uploader Cargo and Semantic
venv * Isolated environments Cargo
pyinstaller Generate Standalone Executables Cargo
pyenv Install and manage versions of language rustup
sphinx Generate documentation from code rustdoc and Cargo
python Interpreter / Compiler rustc and Cargo
ipython REPL rusti
ipdb Debugger rust-gdb

Libraries and Frameworks

Python Definition Rust
urllib * HTTP calls hyper
requests simplified HTTp calls reqwest
json JSON Parsing loading and dumping serde
pyYAML YAML Parsing loading and dumping serde
lxml XML Parsing loading and dumping RustyXML
csv * CSV parsing rust-csv
datetime * & Dateutils Date & time Chrono
click and argparse CLI Framework clap
docopt CLi Framework docopt
re * Regular Expressions regex
subprocess * Run external commands crossbeam and Rayon
logging * Logging log
Pathlib * Path manipulation fs and fs_extra
cryptography crytography crypto
pickle * Object Serialization RON
heapq * Heap Queue BinaryHeap *
bottle Minimal Web Framework Iron
flask Web Framework Rocket
django Full Stack Web Framrwork Gotham
SQL Alchemy Relational Database ORM Diesel
Pymongo Mongo DB driver mongodb
Jinja 2 Template Engine Tera
pygtk GTk desktop development gtk
pyside QT desktop development rust-qt
pygame 2D UI library / gaming Conrod & Piston
unitest2 Test framework Builtin
nose Test Runner Cargo
pytest Testing Framework and Runner Polish
Flake8 Linter Clippy
autopep8 Auto formatter rustfmt
twisted Network application framework libpnet
AsyncIO * Async application framework Tokio and futures
Pillow Image Manipulation Image
Beautiful Soup HTML Parser html5ever
Hypothesis Data Driven test framework proptest
mock Test Mocking Mockito
bioPython Bioinformathics libraries Rust Bio
Dynaconf Config management Config
itertools * Data Structure Iteration Rust Itertools
Geopython geo Spatial Data Geo Rust
ScikitLearn Machine Learning rusty-machine
mistune Markdown / Common Mark Parser cmark
celery Distributed Computation Antimony
boto AWS clients rusoto
AstroPy Astronomy atro-rust
Numpy Numeric Numeric

Applications

Python Definition Rust
Pelican Static Site generator Cobalt
ansible Infra Orchestration realize
mkdocs Generate documentation and e-books from Markdown mdBook
locust HTTP load test drill
Nameko Microservices Framework fractalide
Quokka CMS CMS NIckel CMS

Useful crates

Add Pythonic features to Rust

Python Definition Rust
{'foo': "bar"} Syntax to create a dict / hashmap maplit
__init__(self, value='default') Initializing instances with default values derive_new
itertools *stdlib Extra iterators methods itertools

Show me The code

From Python to Rust by examples

You can copy-paste and run the Rust examples in https://play.rust-lang.org/ and Python in https://repl.it/languages/python3

Creating a new project

Create a new project with baseic files, entry points, module initializer, dependency and installation artifacts.

Python

$ mkdir {pyproject,pyproject/src}
$ touch {pyproject/src/{__init__.py,__main__.py,program.py},pyproject/{requirements.txt,setup.py}} 
$ echo "-e ." >> pyproject/requirements.txt
$ echo "from setuptools import setup" >> pyproject/setup.py
$ echo "setup(author=..., name=...)" >> pyproject/setup.py

Rust

$ cargo new my-rust-program

Installing new libraries/crates

Python

$ pip install foo

Rust

$ cargo install foo

Running / Compiling

Python

$ python my_python_program.py

Rust

$ cargo run

Hello World

Python

if __name__ == "__main__":
    print("Hello, World")

Rust

fn main() {
  println!("Hello, World");
}

Types and Declarations

Create new objects, values on basic primitive types and also data structures.

Python

age = 80
name = 'daffy'
weight = 62.3
loons = ['bugs', 'daffy', 'taz']
ages = {  # Ages for 2017
    'daffy': 80,
    'bugs': 79,
    'taz': 63,
}

Rust

use std::collections::HashMap;

fn main() {
    let age = 80;
    let name = "daffy";
    let weight = 62.3;
    let mut loons = vec!["bugs", "daffy", "taz"];

    let mut ages = HashMap::new();  // Ages for 2017
    ages.insert("daffy", 80);
    ages.insert("bugs", 79);
    ages.insert("taz", 63);
}

Define a function

Defining a function that takes 2 integer arguments and returns its sum.

Python

def add(a, b):
    "Adds a to b"""
    return a + b

Rust

// Adds a to b
fn add(a: i32, b: i32) -> i32 {
  a + b
}

List/Slice

Creating a list, adding new elements, gettings its length, slicing by index, itarating using for loop and iterating with enumerator.

Python

names = ['bugs', 'taz', 'tweety']
print(names[0])  # bugs
names.append('elmer')
print(len(names))  # 4
print(names[2:])  # ['tweety', 'elmer']

for name in names:
    print(name)

for i, name in enumerate(names):
    print('{} at {}'.format(name, i))

Rust

fn main() {
    let mut names = vec!["bugs", "taz", "tweety"];
    println!("{}", names[0]);  // bugs
    names.push("elmer");
    println!("{}", names.len());  // 4
    println!("{:?}", &names[2..]);  // ["tweety", "elmer"]

    for name in &names {
        println!("{}", name);
    }

    for (i, name) in names.iter().enumerate() {
        println!("{} at {}", i, name);
    }
}

Dict/Map

Create new dictionaries (hash maps), adding new keys and values, changing values, getting by key, checking if a key is containing, etc.

Python


# Creating a new dict and populating it
ages = {}
ages['daffy'] = 80
ages['bugs'] = 79
ages['taz'] = 63

# or doing the same using a for loop
ages = {}
for name, age in [("daffy", 80), ("bugs", 79), ("taz", 63)]:
    ages[name] = age

# or initializing from a list
ages = dict([("daffy", 80), ("bugs", 79), ("taz", 63)])

# or passing key values on creation
ages = {  # Ages for 2017
    'daffy': 80,
    'bugs': 79,
    'taz': 63,
}

ages['elmer'] = 80
print(ages['bugs'])  # 79
print('bugs' in ages)  # True

del ages['taz']

for name in ages:  # Keys
    print(name)

for name, age in ages.items():  # Keys & values
    print('{} is {} years old'.format(name, age))

Rust

use std::iter::FromIterator;
use std::collections::HashMap;

fn main() {

    // Creating a new HashMap and populating it
    let mut ages = HashMap::new();  // Ages for 2017
    ages.insert("daffy", 80);
    ages.insert("bugs", 79);
    ages.insert("taz", 63);

    // or doing the same using a loop
    let mut ages = HashMap::new();
    for &(name, age) in [("daffy", 80), ("bugs", 79), ("taz", 63)].iter() {
        // For non-Copy data, remove & and use iter().clone()
        ages.insert(name, age);
    }

    // or initializing from Array
    let mut ages: HashMap<&str, i32> =  // Ages for 2017
        [("daffy", 80), 
         ("bugs", 79), 
         ("taz", 63)]
        .iter().cloned().collect();

    // or initializing from Vec (Iterator)
    let mut ages: HashMap<&str, i32> =  // Ages for 2017
        HashMap::from_iter(
            vec![
               ("daffy", 80),
               ("bugs", 79),
               ("taz", 63)
            ]
        );

    ages.insert("elmer", 80);
    println!("{}", ages["bugs"]);  // 79
    println!("{}", ages.contains_key("bugs")); // true
    ages.remove("taz");


    for name in ages.keys() {  // Keys
      println!("{}", name);
    }

    for (name, age) in &ages {  // Keys & values
      println!("{} is {} years old", name, age);
    }

}

Pythonic alternative to dict/map in Rust

You can use the maplit crate to load hashmap! macro to have an efficient sugared (a.k.a Pythonic) syntax!

# Cargo.toml
[dependencies]
maplit = "*"

then

#[macro_use] extern crate maplit;

let map = hashmap!{
    "daffy" => 80,
    "bugs" => 79,
    "taz" => 63,
};

set / HashSet

Create a set (a hash of unique keys), add new keys and compute intersection, difference and union

Python


# creating and populating
colors = set()
colors.add("red")
colors.add("green")
colors.add("blue")
colors.add("blue")

# using literal syntax
colors = {'red', 'green', 'blue', 'blue'}

# from an iterator
colors = set(['red', 'green', 'blue', 'blue'])


# deduplication
print(colors)  # {"blue", "green", "red"}

# operations
colors = {'red', 'green', 'blue', 'blue'}
flag_colors = {"red", "black"}

# difference
colors.difference(flag_colors)  # {'blue', 'green'}

# symmetric difference
colors.symmetric_difference(flag_colors)  # {'black', 'blue', 'green'}

# intersection
colors.intersection(flag_colors)  # {'red'}

# unioin
colors.intersection(flag_colors)  # {'black', 'blue', 'green', 'red'}

Rust

use std::collections::HashSet;
use std::iter::FromIterator;

fn main() {

    // creating and populating - type inference
    let mut colors = HashSet::new();
    colors.insert("red");
    colors.insert("green");
    colors.insert("blue");
    colors.insert("blue");

    // from an iterator - explicit type
    let mut colors: HashSet<&str> = HashSet::from_iter(vec!["red", "green", "blue", "blue"]);

    // deduplication
    println!("{:?}", colors); // {"blue", "green", "red"}

    // Operations
    let mut colors: HashSet<&str> = HashSet::from_iter(vec!["red", "green", "blue", "blue"]);
    let mut flag_colors: HashSet<&str> = HashSet::from_iter(vec!["red", "black"]);

    // difference
    colors.difference(&flag_colors); // ["green", "blue"]

    // symmetric difference
    colors.symmetric_difference(&flag_colors); // ["blue", "green", "black"]

    // intersection
    colors.intersection(&flag_colors); // ["red"]

    // union
    colors.union(&flag_colors); // ["red", "blue", "green", "black"]
}

or syntax sugared using maplit crate

#[macro_use] extern crate maplit;

let colors = hashset!{"red", "green", "blue", "blue"};

while and for loops

Looping until a condition is met or over an iterable object.

Python

# While loop

counter = 0
while counter < 10:
    print(counter)
    counter += 1

# infinite while loop
while True:
    print("loop Forever!")

# infinite ehile loop with break
counter = 0
while True:
    print(counter)
    counter += 1
    if counter >= 10:
        break


# while loop with continue
counter = 0
while True:
    counter += 1
    if counter == 5:
        continue
    print(counter)
    if counter >= 10:
        break

# For loop over a list
for color in ["red", "green", "blue"]:
    print(color)

# Enumerating indexes
for  i, color in enumerate(["red", "green", "blue"]):
    print(f"{color} at index {i}")

# For in a range
for number in range(0, 100):
    print(number)  # from 0 to 99

Rust

fn main() {

    // While loop
    let mut counter = 0;
    while counter < 10 {
        println!("{}", counter);
        counter += 1;
    }

    // infinite while loop
    loop {
        println!("Loop forever!");
    }

    // infinite while loop with break
    let mut counter = 0;
    loop {
        println!("{}", counter);
        counter += 1;
        if counter >= 10 { break; }
    }

    // infinite while loop with continue
    let mut counter = 0;
    loop {
        counter += 1;
        if counter == 5 { continue; }
        println!("{}", counter);
        if counter >= 10 { break; }
    }

    // for loop over a list
    for color in ["red", "green", "blue"].iter() {
        println!("{}", color);
    }

    // Enumerating indexes
    for (i, color) in ["red", "green", "blue"].iter().enumerate() {
        println!("{} at index {}", color, i);
    }

    // for in a range
    for number in 0..100 {
        println!("{}", number);  // from 0 to 99
    }
}

Loop Labels

Rust has a looping feature which is not present on Python: Loop labels

'outer: for x in 0..10 {
    'inner: for y in 0..10 {
        if x % 2 == 0 { continue 'outer; } // continues the loop over x
        if y % 2 == 0 { continue 'inner; } // continues the loop over y
        println!("x: {}, y: {}", x, y);
    }
}

Files

Read a text file and iterate its lines printing the content, properly close the file at the end.

Python

from pathlib import Path

with open(Path("/tmp/song.txt")) as fp:
    #  Iterate over lines
    for line in fp:
        print(line.strip())

Rust

use std::io::{BufReader, BufRead};
use std::fs::File;
use std::path::Path;


fn main () {
    let fp = File::open(Path::new("/tmp/song.txt")).unwrap();
    let file = BufReader::new(&fp);
    for line in file.lines() {
        //  Iterate over lines
        println!("{}", line.unwrap());
    }
}

Exceptions/Return Error

Expecting for exceptions and identifying errors.

Python

def div(a, b):
    if b == 0:
        raise ValueError("b can't be 0")
    return a / b

# ...

try:
    div(1, 0)
except ValueError:
    print('OK')

Rust

```

---

### Concurrency

**Python**

```python
thr = Thread(target=add, args=(1, 2), daemon=True)
thr.start()

Rust

```

---

### Communicating between threads

Managing data context between threads.

**Python**

```python
from queue import Queue
queue = Queue()
# ...
# Send message from a thread
queue.put(353)


# ...
# Get message to a thread
val = queue.get()

Rust

```

---

### Sorting

Sorting lists, reversing and using a key.

**Python**

```python
names = ['taz', 'bugs', 'daffy']

# Lexicographical order
names.sort()

# Reversed lexicographical order
names.sort(reverse=True)

# Sort by length
names.sort(key=len)

Rust

```

---

### Web app with Flask / Rocket

**Python**

```python
from flask import Flask

app = Flask(__name__)


@app.route('/')
def index():
    return 'Hello Python'


if __name__ == '__main__':
    app.run(port=8080)

Rust

#![feature(plugin)]
#![plugin(rocket_codegen)]

extern crate rocket;

#[get("/")]
fn index() -> &'static str {
    "Hello Rust"
}

fn main() {
    rocket::ignite().mount("/", routes![index]).launch();
}

MISSING SOME IMPLEMENTATIONS AND EXAMPLES PLEASE CONTRIBUTE on https://github.com/rochacbruno/py2rs

HTTP Request with error handling

Python

import json
from urlib2 import urlopen

url = 'https://httpbin.org/ip'
try:
    fp = urlopen(url)
except HTTPError as err:
    msg = 'error: cannot get {!r} - {}'.format(url, err)
    raise SystemExit(msg)

try:
    reply = json.load(fp)
except ValueError as err:
    msg = 'error: cannot decode reply - {}'.format(err)
    raise SystemExit(msg)

print(reply['origin'])

Rust

```

---

### Encode and Decode JSON

**Python**

```python
data = '''{
    "name": "bugs",
    "age": 76
}'''
obj = json.loads(data)

json.dump(obj, stdout)

Rust

```


---


### Print Object for Debug/Log 

**Python**

```python
daffy = Actor(
    name='Daffy',
    age=80,
)
print('{!r}'.format(daffy))

Rust

```

---

### Object Orientation

**Python**

```python
class Cat:
    def __init__(self, name):
        self.name = name

    def greet(self, other):
        print("Meow {}, I'm {}".format(other, self.name))

# ...

grumy = Cat('Grumpy')
grumy.greet('Grafield')

Rust

rust


Credits

NOTE The original repository for this article is on https://github.com/rochacbruno/py2rs CONTRIBUTE

Created by Bruno Rocha @rochacbruno inspired by https://www.353.solutions/py2go/index.html

With contributions by:

  • Send a PR and include your name and links

Simple Login Extension for Flask

Travis PyPI PyPI PyPI Flask

Login Extension for Flask

There are good and recommended options to deal with web authentication in Flask.

I recommend you use:

Those extensions are really complete and production ready!

So why Flask Simple Login?

However sometimes you need something simple for that small project or for prototyping.

Flask Simple Login

What it provides:

  • Login and Logout forms and pages
  • Function to check if user is logged-in
  • Decorator for views
  • Easy and customizable login_checker

What it does not provide: (but of course you can easily implement by your own)

  • Database Integration
  • Password management
  • API authentication
  • Role or user based access control

Hot it works

First install it from PyPI.

pip install flask_simplelogin

from flask import Flask
from flask_simplelogin import SimpleLogin

app = Flask(__name__)
SimpleLogin(app)

That's it! now you have /login and /logout routes in your application.

The username defaults to admin and the password defaults to secret (yeah that's not clever, let's see how to change it)

Configuring

Simple way

from flask import Flask
from flask_simplelogin import SimpleLogin

app = Flask(__name__)
app.config['SECRET_KEY'] = 'something-secret'
app.config['SIMPLELOGIN_USERNAME'] = 'chuck'
app.config['SIMPLELOGIN_PASSWORD'] = 'norris'

SimpleLogin(app)

That works, but is not so clever, lets use env vars.

$ export SIMPLELOGIN_USERNAME=chuck
$ export SIMPLELOGIN_PASSWORD=norris

then SimpleLogin will read those env vars automatically.

from flask import Flask
from flask_simplelogin import SimpleLogin

app = Flask(__name__)
app.config['SECRET_KEY'] = 'something-secret'
SimpleLogin(app)

But what if you have more users and more complex auth logic? write a custom login checker

Using a custom login checker

from flask import Flask
from flask_simplelogin import SimpleLogin

app = Flask(__name__)
app.config['SECRET_KEY'] = 'something-secret'


def only_chuck_norris_can_login(user):
    "user = {'username': 'foo', 'password': 'bar'}"
    # do the authentication here, it is up to you!
    # query your database, check your user/passwd file
    # connect to external service.. anything.
    if user.get('username') == 'chuck' and user.get('password') == 'norris':
       return True  # Allowed
    return False  # Denied


SimpleLogin(app, login_checker=only_chuck_norris_can_login)

Checking if user is logged in


from flask_simplelogin import is_logged_in

if is_logged_in():
    # do things if anyone is logged in

if is_logged_in('admin'):
    # do things only if admin is logged in

Decorating your views

from flask_simplelogin import login_required

@app.route('/it_is_protected')
@login_required   # < --- simple decorator
def foo():
    return 'secret'

Protecting Flask Admin views


from flask_admin.contrib.foo import ModelView
from flask_simplelogin import is_logged_in


class AdminView(ModelView)
    def is_accessible(self):
        return is_logged_in('admin')

Customizing templates

There are only one template to customize and it is called login.html

Example is:

{% extends 'base.html' %}
{% block title %}Login{% endblock %}
{% block messages %}
   {{super()}}
   {%if form.errors %}
     <ul class="alert alert-danger">
       {% for field, errors in form.errors.items() %}
         <li>{{field}} {% for error in errors %}{{ error }}{% endfor %}</li>
       {% endfor %}
     </ul>
   {% endif %}
{% endblock %}

{% block page_body %}
       <form action="{{ url_for('simplelogin.login', next=request.args.get('next', '/')) }}" method="post">
            <div class="form-group">
            {{ form.csrf_token }}
            {{form.username.label}}<div class="form-control">{{ form.username }}</div><br>
            {{form.password.label}}<div class="form-control"> {{ form.password }}</div><br>
            </form>
           <input type="submit" value="Send">
       </form>
{% endblock %}

Take a look at the example app.

And you can customize it in anyway you want and need, it receives a form in context and it is a WTF form the submit should be done to request.path which is the same /login view.

You can also use {% if is_logged_in %} in your template if needed.

Requirements

  • Flask-WTF and WTForms
  • having a SECRET_KEY set in your app.config

Publish your Python packages easily using flit

Deploying Python Packages to PyPI using Flit

The traditional way of package deployment in Python is using a setup.py script located in the root of your project and then running python setup.py sdist upload to release a new version.

It works by using distutils, setuptools or distribute and there is also twine which is a command line application to manage uploads.

History

distutils is the standard way of Python package distribution included in standard library since Python 2.x then setuptools was created to overcome distutils limitations and also introduces a command line application called easy_install (currently we use its sucessor called pip) and also setuptools introduced a very handy feature called pkg_resources. One of the characteristics of setuptools is that it uses Monkey Patching over the standard distutils to fix existing problems.

Other forks of setuptools has been created to fix that issues and add common developers preferences so well known forks like distribute and distutils2 and distlib has been merged back to the original setuptools

Lots of other packaging tools has been created to try to fix the distribution problems, some maintained by PyPA (Python Package Authority) and some maintained by community.

How it works in standard way

using one of the above you should create a file called setup.py in the root of your project, e.g:

from <my_favorite_dist_tool> import setup

# Example taken from Django's repository
setup(
    name='Django',
    version=version,
    url='https://www.djangoproject.com/',
    author='Django Software Foundation',
    author_email='foundation@djangoproject.com',
    description=('A high-level Python Web framework that encourages '
                 'rapid development and clean, pragmatic design.'),
    license='BSD',
    packages=find_packages(exclude=EXCLUDE_FROM_PACKAGES),
    include_package_data=True,
    scripts=['django/bin/django-admin.py'],
    entry_points={'console_scripts': [
        'django-admin = django.core.management:execute_from_command_line',
    ]},
    install_requires=['pytz'],
    extras_require={
        "bcrypt": ["bcrypt"],
        "argon2": ["argon2-cffi >= 16.1.0"],
    },
    zip_safe=False,
    classifiers=[
        'Development Status :: 2 - Pre-Alpha',
        'Environment :: Web Environment',
        'Framework :: Django',
        'Intended Audience :: Developers',
        'License :: OSI Approved :: BSD License',
        'Operating System :: OS Independent',
        'Programming Language :: Python',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.4',
        'Programming Language :: Python :: 3.5',
        'Programming Language :: Python :: 3.6',
        'Topic :: Internet :: WWW/HTTP',
        'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
        'Topic :: Internet :: WWW/HTTP :: WSGI',
        'Topic :: Software Development :: Libraries :: Application Frameworks',
        'Topic :: Software Development :: Libraries :: Python Modules',
    ],
)

As you can see it is very confusing to decide which of the distribute tools to addopt and how the setup.py should be writen because there are different examples over the github most famous Python repositories.

Making things easier with Flit

NOTE: Forget all about all that history and setup.py you read above and consider using only Flit.

Flit is a simple way to Package and deploy Python projects on PyPI, Flit makes it easier by using a simple flit.ini file and assumes common defaults to save your time and typing.

I knew about Flit when I was taking a look at Mariatta Wijaya game called Tic Tac Taco Pizza and noticed that she used flit to deploy the game, so we also asked her the reason for using this on the podcast we recorded so I decided to try porting my projects to Flit.

How it works?

Instead of a complex setup.py you put a simple flit.ini in the root of your project and it looks like:

[metadata]
module = markdocs
author = Bruno Rocha
author-email = rochacbruno@gmail.com
maintainer = rochacbruno
maintainer-email = rochacbruno@gmail.com
home-page = https://github.com/rochacbruno/markdocs
requires = mistune
           click
description-file = README.md
classifiers = Programming Language :: Python :: 3.6
              Intended Audience :: Developers
              License :: OSI Approved :: MIT License
              Topic :: Documentation
              Topic :: Software Development :: Documentation
              Topic :: Software Development :: Quality Assurance
requires-python = >=3.6

[scripts]
markdocs = markdocs:main

Now you only need to have flit installed in your local machine pip3 install flit (unsing pip3 as flit works only in Python3+) and optionally is recommended to have pandoc and pypandoc installed because Flit can convert your README.md into the .rst, the format still used by PyPI (note that Markdown support is coming to PyPi.org soon).

The advantages are:

  • No more complicated setup.py
  • If you omit some fields it will assume common used defaults
  • It is easier to read and write
  • Will convert your README.md
  • Will take the __version__ included in your program/__init__.py
  • Can deploy to TestPyPI
  • Avoids over engineering on setup.py

Development installation

To install your package during development use

flit install --symlink --python path/to/virtualenv/bin/python

the --python is optional, by default will take the current which python

Registering and deploying

It is easy and will register the new project for you if doesn't exist on PyPI

flit publish

Flit packages a single importable module or package at a time, using the import name as the name on PyPI. All subpackages and data files within a package are included automatically.

Important!

  • Flit will use the data from ~/.pypirc to authenticate and to find the server deployment addresses
  • You can also set FLIT_USERNAME and FLIT_PASSWORD and FLIT_INDEX_URL as environment variables which makes flit good for CI deployment (e.g: TravisCI)

What is missing?

NOTE: Flit is open-source, so some of this things are already under consideration and there are PRs opened.

  • Flit will not bump your project version automatically, you can still use tools like bumpversion but this feature would be better if builtin
  • Flit will not parse a requirements.txt file and would be nice to have it as tools like pyup.io can track those files but not flit.ini yet
  • Flit does not create a .lock frozen file with the version used on specific release and it is interesting just like pipenv does

Conclusion

Flit is the easier way to deploy packaged to PyPI following 3 simple steps.

  1. Install flit
  2. Describe your flit.ini
  3. Run flit publish

then your library is released to PyPI.

However

Python still needs better standards because you still need separated tools to make common tasks and using a single tool to that tasks (pack, install, deploy, create) would be better (just like what cargo does for Rust), instead in Python you have:

  • Flit to deploy packages to PyPI for distribution
  • bumpversion to bump your semver number
  • pip to install and update packages from PyPI (or pipenv/WIP to do the same with more powers)
  • Cookiecutter to create a new Python Package (from strucured templates)
  • safety to check dependencies security
  • flake or pylint to static and styling checks
  • venv, pyenv or virtualenvwrapper to manage isolated environments
  • pytest to run tests
  • pyup.io to watch for depdendency updates
  • tox for testing on multiple environments
  • sphinx to create documentation
    • lots of other options

Having so many tools brings a lot of confusion and makes it hard to choose, why not having a single tool, based on different plugins sharing the same API?

Just an idea

python -m manage [install packagename]               # <-- calls pip or pipenv
                 [publish --options --bump=version]  # <-- calls `flit` and `bumpversion`
                 [new packagename templatename]      # <-- calls cookiecutter
                 [safecheck]                         # <-- calls safety
                 [checkupdates]                      # <-- checks in the same way as Pyup.io does
                 [test path]                         # <-- calls pytest, nose or unittest
                 [lint path]                         # <-- calls flake, pylint 
                 [venv options]                      # <-- calls the existing venv module
                 [docs --options]                    # <-- calls sphinx, pydoc, markdocs or other

All above configurable via config file or env vars and each of that endpoints would be provided by many plugins sharing the same API, so you could choose between flit or twine as your publish manager etc..

So maybe I can implement that features in manage

Please share in comments if you know some other Python management tool

Migrando e-commerce do Iluria para o Shopify (usando Python)

Iluria

Ilúria é uma empresa brasileira de e-commerce que fornece uma plataforma bastante interessante para quem está começando uma loja virtual e precisa de algo simples e funcional. O sistema do Ilúria é realmente simples e de fácil uso, por isso continuo recomendando essa plataforma caso a sua necessidade seja básica e seu negócio ainda estiver começando.

Porém quando você começar a ter necessidades mais específicas, personalizações no sistema de sua loja e melhor colocação nas buscas o Ilúria infelizmente deixará de te atender e surgirá a necessidade de migrar para uma plataforma mais completa.

Vantagens do Iluria

  • É uma plataforma brasileira!
    • E isso ajuda a obter suporte.
    • É legal colaborar com o crescimento de empresas nacionais!
  • O preço é bom!
    • Eles fornecem 15 dias grátis
    • Para um catálogo de 50 produtos custa R$ 9,90, 200 produtos R$ 29,90 e o preço vai aumentando de acordo com a quantidade produtos cadastrados.
  • É fácil de usar
    • A plataforma tem uma admin bastante simples e fácil de usar.

Desvantagens do Iluria

  • GOOGLE & SEO
    • Apesar de o site dizer que é otimizado para o Google, o Iluria não fornece muitas opções de fácil acesso para otimizar o SEO da loja, não tem area para customizar URLs, descriptions, tags, sitemap etc... e isso seria possível apenas programando o template.
    • As configurações padrão de SEO não são suficientes para uma boa colocação no Google.
  • Falta de relatórios analíticos
    O Iluria não oferece muitos relatórios analíticos e isso torna bastante difícil fazer re-marketing.
    • Carrinho abandonado
      O Iluria não tem controle de carrinho de compras abandonado e isso impossibilita que você lembre seu cliente sobre compras esquecidas, e está é uma das melhoras práticas para recuperar vendas.
    • Buscas
      Outro relatório interessante seria o relatório de buscas, o cliente entra na sua loja e digita na busca "azul" mas não encontra as camisetas azuis que você tem no seu catálogo e então vai para outra loja. O iluria deveria ter um relatório para te informar isso, pois dessa forma você pode melhorar a descrição e tags de seus produtos que da próxima vez a "camiseta azul" seja encontrada!
  • Não tem API
    Essa é gravíssima, injustificável, imperdoável !!!!
    Estamos em 2017 e o mundo da web gira em torno de API qualquer possibilidade de estender, criar plugins, melhorar seria através de APIs, eu enviei um e-mail para o Iluria perguntando e disseram que simplesmente não tem nenhum tipo de API e isso é injustificável para uma empresa já estabelecida como a Iluria. Sem API os problemas são:
    • Não é possível estender as funcionalidades da plataforma.
    • Não é possível programar web-hooks para disparar envios de e-mails por exemplo usando IFTTT, Zapier e outras tecnologias de automação.
    • Não é possível cadastrar produtos em massa que você já tenha em seu banco de dados ou planilha.
    • Não é possível integrar com market places como MercadoLivre e Buscapé.
    • Não ajuda nem na hora de migrar para outra plataforma como vocês verão nos códigos a seguir.
  • Seus dados não te pertencem
    Bom, pelo menos é o que parece, e isso me leva a creditar que a falta de uma API possa fazer parte de alguma estratégia do Iluria para não permitir a saída dos clientes.
    • Exportar lista de clientes e produtos é limitada. No admin até existem as opções exportar para a lista de produtos e clientes, mas as informações exportadas não são suficientes. (mostrarei em seguida)

[IMG API]

Eu ainda continuo recomendando o Ilúria para quem está começando no e-commerce é com certeza a plataforma mais acessível. Mas realmente gostaria que essa empresa abrisse os olhos para a oportunidade que eles tem em mãos e trabalhassem para oferecer mais funcionalidades para os clientes, evitando assim que abandonem a plataforma quando começarem a crescer e eu ficaria mais feliz em estar aqui falando apenas das vantagens de uma plataforma nacional de e-commerce.

Preciso migrar e agora?

Na hora que precisar migrar para outra plataforma você precisa ter certeza de que o seu histórico não será perdido, e no caso do Iluria isso é em difícil pois as opções existentes não fornecem muitos dados.

O que você precisa manter:

  • Cadastro de produtos e variantes (contendo imagens)
  • Histórico de vendas
  • Cadastro de clientes

O primeiro desafio é conseguir esses dados, no caso do projeto em que eu trabalhei na migração decidimos não migrar nem o cadastro de clientes (pois já existia um cadastro em paralelo no MailChimp) e nem o histórico de vendas (pois é possível ter esta informação no gateway de pagamento).

Portanto o que posso mostrar aqui neste post é como migrar a sua lista de produtos cadastrados e para isso utilizei a lista limitada fornecida pelo Iluria como ponto de partida e também um script em Python para pegar os dados dos produtos via crawler.

Para exportar a lista de produtos utilize o menu relatórios -> estoque de produtos conforma a imagem abaixo:

[IMG EXPORT PROD]

Você irá baixar um arquivo .csv com o seguinte formato:

Produto;Nome;Varia��o 1;Varia��o 2;Varia��o 3;Estoque;Pre�o;Pre�o de custo;Nome do fornecedor
3F553C;Madeira 147;1,40 x 1,40;;;Sob encomenda;160,00;;
3F553B;Diversos 115;1,40 x 1,40;;;Sob encomenda;160,00;;
3F553B;Diversos 115;1,40 x 2,00;;;Sob encomenda;220,00;;

Você deve estar se perguntando o porquê dos caracteres no exemplo acima?

Bom como se já não bastasse tudo o que relacionei acima o Iluria ainda surpreende com mais isso, OS DADOS ESTÃO em codificação ISO-8859-1 e mais uma vez me perguntei em que ano estamos? porquê não está em UTF-8??

E as minhas imagens?

Bom, agora que entra a parte divertida, como percebem o arquivo que exportamos acima não traz muita informação sobre o produto, não tem o texto de descrição e também não tem uma referência para a imagem do produto, portanto resolveremos isso com Python!!!

Python FTW

Puxando os dados do Iluria através de crawling

O código dessa parte é bem simples (pode melhorar) mas o que apresento aqui é o que funcionou para mim nesta migração:

primeiro vamos criar um arquivo chamado utils.py

# coding: utf-8
import csv
import shutil
import requests
from bs4 import BeautifulSoup


def get_image_and_description(produto, link):
    """Baixa a imagem do iluria e salva no diretório atual
    Pega o texto de descrição do produto e retorna
    se não encontrar retorna None.
    """

    user_agent = {'User-agent': 'Mozilla/5.0'}
    response = requests.get(link, headers = user_agent)
    if response.status_code != 200:
        return
    soup = BeautifulSoup(response.content, "html.parser")

    image_element = soup.find("img", {"id": "big-image"})
    if image_element:
        image_url = "http:{0}".format(image_element['src'])
        image_url = image_url.replace("450xN", "850xN")

        image_content = requests.get(image_url, stream=True)
        if image_content.status_code == 200:
            filename = "{0}.jpg".format(produto)
            with open(filename, "wb") as image_file:
                shutil.copyfileobj(image_content.raw, image_file)
            del image_content

    description_element = soup.find(
        "div", {"class": "product-description"}, text=True)

    if description_element:
        return description_element[0]


# HACK ALLERT!!
# A função abaixo "imita" uma classe
def IluriaDictReader(data, **kwargs):
    """Lê o csv do Iluria em ISO-8859-1"""
    csv_reader = csv.DictReader(data, **kwargs)
    for row in csv_reader:
        yield {
          key: value.decode('iso-8859-1').encode('utf8')
          for key, value in row.iteritems()
        }

A primeira função acima irá fazer o download da imagem do Iluria e também salvar a descrição do produto já que essas informações não tem no csv exportado e caso você precise de outras informação basta adicionar mais elementos ao soup.find e retornar os dados.

Até aqui com as 2 funções acima já é possível imaginar em como fazer uma migração mas agora você tem que decidir para qual plataforma migrar, vamos falar delas!

Para onde ir?

Para este projeto analisamos algumas alternativas ao Iluria e tentamos dar preferência a alternativas nacionais e vamos falar delas.

  • Box Loja Essa pareceu ser uma opção bem próxima ao Iluria, os preços são bons entre 20 e 50 por mês de acordo com a quantidade de produtos, não cobram taxas por cada venda efetuada e parece ter algumas facilidades para customização. Porém mais uma vez o que tirou essa plataforma da lista de candidatos foi a falta e API, eu vasculhei o site deles, fóruns e Google, vi até em sites de freelancers pessoas procurando quem fizesse isso via algum script robô para migrar dados de Magento para o Box loja pelo fato deles ainda não terem API. E no rodapé do site deles não tem um link bem claro escrito developers ou API, e isso foi motivo suficiente para eliminar apesar de parecer uma boa opção.

  • Loja Integrada (recomendado !!!)
    Tá aqui mais uma opção que parece ser fantástica, e como nome sugere eles fornecem uma API!!! e por isso estão de parabéns!!! Esta plataforma é muito bem falada, e realmente parece uma plataforma de e-commerce que está a frente dos concorrentes, eu gostaria muito de ter migrado este projeto para o Loja Integrada, porém aqui pesou a questão comercial, os preços do Loja Integrada ainda não são tão convidativos quanto dos concorrentes acima, e isso parece ser justo pois oferecem mais vantagens, porém neste ponto aqui o Shopify acabou ganhando

Shopify

Shopify é uma empresa Canadense que oferece uma das mais utilizadas plataformas de e-commerce do mundo (supostamente 150 mil lojas) é escrita em Ruby on Rails, mas apesar disso possui uma API bastante completa e muito bem documentada. Além disso o Shopify criou o Liquid uma linguagem de template bastante fácil e inspirada no já conhecido Jinja portante muito confortável para qualquer programador Python interagir.

enter image description here

Vantagens do Shopify

  • Confiável
    • Uma empresa do porte do Shopify mantém um suporte bastante ativo e o minimo que podemos esperar é uma plataforma estável e confiável.
  • Integrada
    • A API do Shopify é muito bem escrita e com documentação completa tornando fácil interagir tanto para importar e exportar dados, quanto para criar aplicações que estendam as funcionalidades.
  • APPs
    • Por conta da API citada acima, o Shopify oferece um market place de APPs é possível encontra ruma variedade de plugins tando gratuitos quanto comerciais para instalar na sua loja e você também pode usar a API para criar seus próprios APPs.
  • Temas!!!
    • Isso é muito importante e o Shopify parece ter feito da forma certa, pois como já mencionei a linguagem de templates é fácil de usar então isto resulta em muitos templates disponíveis gratuitamente e também empresas como Envato, Themes Monster etc oferecendo várias opções comerciais, e isto também fácil para encontrar desenvolvedores para customizar temas.
  • SEO, Google, Buscas
    • Essa é uma parte muito interessante, de maneira simples é possível customizar as opções de SEO e os resultados são muito bons, poucas horas depois de migrar já tínhamos resultados diferentes no Google. (analisados através do Google developer tools)
  • Relatórios
    • O Shopify oferece desde o plano mais básico alguns relatórios essenciais como o controle de carrinhos abandonados, buscas efetuadas, balanço de vendas etc.. E os planos mais superiores ainda oferecem relatórios customizados.
  • Smart Collections
    • Aqui está outro recurso interessante, para quem tem muitos produtos e não quer ficar organizando manualmente é possível criar regras para que os produtos sejam automaticamente colocados em determinadas categorias/menus usando condições simples como sempre que aparecer 'camiseta' no título colocar este produto na categoria 'roupas', etc...
  • Preço
    • O plano básico do Shopify custa 30 dólares (pouco mais de 100,00) e oferece um grande número de funcionalidades e ainda é possível incluir APPs para obter ainda mais recursos.
  • Biliotecas
    • O próprio Shopify mantém bibliotecas em algumas linguagens como Ruby, Java, C#, Python e PHP para interagir com a API deles :)

Desvatagens do Shopify

  • Admin em Inglês
    • Os temas de front-end podem ser traduzidos para qualquer lingua, mas o admin apenas em inglês e isso dificulta bastante a adoção mesmo para quem fala a lingua inglesa pois alguns termos como "fulfillment" não são de fácil tradução.
  • Complexidade do Admin
    • Este problema só ocorre na primeira semana de uso, em pouco tempo você já se acostuma com a UI do admin, porém nas primeiras horas navegando você irá soltar muitos "What The Fuck???"
  • Compatibilidade com a realidade brasileira
    • O Shopify já atende bem tudo o que uma loja brasileira precisa, porém é bem claro que estão preparados para um estilo diferente de comércio, algumas coisas como taxas, estoque etc são feitas de um modo que não é usual no Brasil mas isso acredito que seja mais um problema de adaptação pois talvez nossos comércios que precisem se adaptar a um esquema mais organizado, e isso exige tempo e paciência.
  • Meios de pagamento
    • É possível configurar Paypal, PagSeguro, Mercadopago, Moip, Bitcoins entre outros. Eles tem o sistema preparado para trabalhar bem com todas essas plataformas, porém você só pode escolher Paypal + 1 para ser ativado em sua loja simultaneamente, ou seja, Paypal + Pagseguro, ou Paypal + Mercado Pago. Não é possível dar opção de seu cliente escolher qual gateway deseja usar.
  • Correios só através de app
    • Até existe um cálculo de correio integrado, mas é por faixa de CEP e você precisa configurar os preços manualmente, para ter um frete automático você precisa usar um APP adicional, é muito fácil de instalar, basta clicar em um botão! mas você tem que pagar + 5 dólares por mês para usar.

Mesmo com as desvantagens listadas acima o Shopify pareceu uma boa escolha, e a empresa está ativamente respondendo questões de brasileiros no seu fórum indicando que logo irão implementar mais facilidades e resolver essas limitações.

Decidimos migrar para o Shopify!!!

Interagindo com a API do Shopify via Python

Apesar do Shopify manter uma biblioteca Python para interagir com a API deles, eu analisei e achei que a solução mantida por eles não é muito Pythonica então continuei procurando.

Encontrei o projeto Python-Shopify que ainda não estava totalmente funcional e então fiz um fork e comecei a contribuir, arrumei alguns bugs e fiz o release para o PyPI. portanto agora é possível usar com pip install python-shopify.

Portanto em nosso projeto agora é a hora de criar o código que vai popular os produtos no banco de dados do Shopify usando aqueles dados que extraímos no código que mostrei acima.

Rode pip install python-shopify slugify tqdm e então no arquivo api.py

import os
from slugify import slugify
from shopify.products import (
    ProductsApiWrapper, Product, Image, Variant, Option
)


# FILL THE DATA below with data generated in Shopify-> admin -> apps
api_key = ''  
password = '' 
store_name = 'sua-loja'

paw = ProductsApiWrapper(api_key, password, store_name)

# Get a list of existing products, limited to 250 :(
existing = [item.title for item in paw.list(limit=250)]


def create_product(items):
    """Items is a list of dictionaries representing each product variant
    of the same product with the same ID and other data
    keys: ['description', 'price', 'name', 'link', 'size', 'stock']
    items = [
        # first variant holds full data and is default
        {'name': 'Awesome t-shirt',
         'code': '123456',
         'description': '<html>',
         'size': 'P',
         'price': '22.5',
         'stock': 2},
        # Other variants
        {'size': 'M',
         'price': '25.5',
         'stock': 2},
        {'size': 'G',
         'price': '29.5',
         'stock': 0},
    ]
    """

    # The first item should be the complete item holding all the fields
    # other items can have only the variants
    data = items[0]

    # Iluria gives us ISO-8859-1 :(
    name = data['name'].decode('utf-8')


    if name in existing or paw.list(title=name):
        # skip existing
        print "Already registered, skipping..."
        # or perform an update!!!
        return

    product = Product(
        title=data['name'],
        body_html=data['description'],
    )

    # There should be a 123456.jpg file in the same folder
    # alternatively you can use a URL provided in data
    image_filename = "{0}.jpg".format(data['code'])
    if os.path.exists(image_filename):
        image = Image()
        image.attach(image_filename)
        product.add_image(image)
    elif data.get('image_url'):
        product.add_image(Image(src=data['image_url']))

    # using the first word in title as tag
    # Product "T-shirt Blue 09" got tag "t-shirt"
    tag = data['name'].split()[0]
    tag = u''.join(i for i in tag if not i.isdigit())

    product.add_tags(tag.strip().lower())

    # You can add only 3 options
    # at positions 1, 2 and 3
    # you should add options before adding its variants
    product.add_option(
      Option(
        name="Size",
        position=1,
      )
    )

    for item in items:
        product.add_variant(
            Variant(
                option1=item['size'],
                # option2=data['size'],
                # option3=data['size'],
                title="Size",
                price=item['price'],
                # SKU should be different for each variant
                sku=data["code"] + slugify(item['size']), 
                position=1,
                inventory_policy="continue",
                fulfillment_service="manual",
                inventory_management="shopify",
                inventory_quantity=int(item['stock']),
                taxable=False,
                weight=300,
                weight_unit="g", # g, kg
                requires_shipping=True
            )
        )

    try:
        product = paw.create(product)
    except Exception as e:
        # do a proper logging here please!!!
        print e
        print product
        print items

    return product

Eu inclui o exemplo acima no repositório do Python-Shopify

Migrando do Iluria para o Shopify!!!

Agora precisamos juntar nossos 2 arquivos utils.py e api.py em um script e ai rodar a migração dos produtos.

import os
from collections import defaultdict
from tqdm import tqdm

from api import create_product
from utils import get_image_and_description, IluriaDictReader

BASE_URL = "http://www.sua_loja_iluria.com.br/pd-"
reader = IluriaDictReader(open('iluria_produtos_estoque.csv'), delimiter=";")
produtos = defaultdict(list)

for item in reader:
    size = item['Varia\xe7\xe3o 1']
    produtos[item["Produto"]].append(
       {
        "link": "{base}{slug}.html".format(
            base=BASE_URL, slug=item['Produto']
        ),
        "name": item["Nome"],
        "size": size,
        "price": item['Pre\xe7o'].replace(",", "."),
        "stock": item['Estoque'],
        "code": item['Produto']
       }
    )

for produto, items in tqdm(produtos.items()):
    data = items[0]  # cada item é uma lista com variações

    # pegamos a descrição e já fazemos o download da imagem
    # idealmente teriamos 2 funções, mas estamos só hackeando!!! :)
    data['description'] = get_image_and_description(
        data['code'], data['link']
    )

    if not os.path.exists('{0}.jpg'.format(produto)):
        # sem imagem sem cadastro!!!
        continue

    if not data['name']:
        # name é obrigatório
        continue

    # criamos o produto na API do Shopify e success!!!
    create_product(items)

Após o término do script você terá seus produtos cadastrados no Shopify e então os próximos passos será escolher e customizar seu tema (ou criar um próprio) e ai configurar sua loja, dominios, frete etc..

Apps recomendados:

Conclusão

Iluria uma empresa que tem uma boa plataforma mas que está perdendo a oportunidade de se tornar a maior plataforma de e-commerce brasileira por simplesmente não investir em evolução tecnológica de sua plataforma.

Loja Integrada aparentemente a melhor opção para quem quer se manter em uma plataforma brasileira, não tenho mais informações pois não cheguei realmente a utilizar, mas eles poderiam melhorar os preços de entrada, assim iriam atrair as lojas que inevitavelmente irão sair do Iluria

Shopify uma ótima opção, com muita coisa a melhorar para o público brasileiro mas mesmo assim com um pouco de dedicação e leitura das documentações é possível criar uma loja 100% funcional em poucas horas!!!

Para referencia: A loja que migramos no projeto citado é a https://fundosemtecido.com.br/ que comercializa fundos fotográficos para fotógrafos e conseguimos efetuar a migração desde a exportação dos dados, criação de tema personalizado, configurações de admin até colocar no ar em apenas 2 dias.

Links:

Python-Shopify

AsyncIO - O futuro do Python mudou completamente!

Tradução do artigo original escrito por Yeray Diaz para o hackernoon: AsyncIO for the working Python developer

Eu me lembro do exato momento em que eu pensei, "Uau, isso está lento, aposto que se eu pudesse paralelizar essas chamadas isso voaria!" e então, 3 dias após, eu olhei para o meu código e não pude reconhece-lo, havia se transformado em um misturado de chamadas para threading e funções da biblioteca processing.

Então encontrei o asyncio, e tudo mudou!


Se você não conhece, asyncio é o novo módulo de concorrência introduzido no Python 3.4. É projetado para usar coroutines e futures para simplificar a programação assíncrona e tornar o código tão legível quanto o código síncrono simplesmente por não haver callbacks.

Eu também me lembro que enquanto eu estava naquela busca pelo paralelismo inúmeras opções estavam disponíveis, mas uma se destacou. Era rápida, fácil de aprender e muito bem escrita: A excelente biblioteca gevent. Eu cheguei até ela lendo o encantador tutorial mão na massa: Gevent for the Working Python Developer, escrito por uma sensacional comunidade de usuários, uma ótima introdução não apenas ao gevent mas ao conceito de concorrência em geral, e você também deveria dar uma lida.

Eu gostei tanto do tutorial que decidi usa-lo como template para escrever sobre o AsyncIO.

Aviso Rápido: Isto não é um artigo sobre gevent X asyncio, O Nathan Road escreveu a respeito das diferenças e similaridades entre esses 2 se você estiver interessado.

Uma nota a respeito do código neste tutorial, você deve ter lido que no Python 3.5 uma nova sintaxe foi introduzida, especialmente para coroutines, eu estou intencionalmente não utilizando esta nova sintaxe neste texto pois desta forma acredito que fica mais fácil para assimilar as coroutinas com generators. Mas você pode encontrar versões dos exemplos usando esta nova sintaxe no github.

Eu sei que você já deve estar ansioso mas antes de mergulharmos eu gostaria de primeiramente falar rapidamente sobre alguns conceitos que talvez não lhe sejam familiares.

Threads, loops, coroutines and futures

Threads são uma ferramenta comum e a maioria dos desenvolvedores já ouviu falar ou já usou. Entretanto o asyncio usa de estruturas um pouco diferentes: event loops, coroutines e futures.

  • Um event loop gerencia e distribui a execução de diferentes tarefas. Ele mantém um registro de tarefas (coroutines) e distribui o fluxo de execução entre elas.
  • As coroutines são geradores Python (generators), que quando há a ocorrência do yield libera o controle do fluxo de volta ao event loop. Uma coroutine precisa estar programada para ser executada usando o event loop, para fazer isso criamos uma tarefa do tipo future.
  • E um future é um objeto que representa o resultado de uma tarefa que pode, ou não, ter sido executada. Este resultado pode ser uma Exception.

Entendeu? simples né? vamos mergulhar neste conceito!

Execução síncrona e Execução assíncrona

Em Concorrência não é paralelismo, é melhor! o Rob Pike falou uma coisa que fez um click na minha cabeça: Dividir tarefas em sub-tarefas concorrentes já é o suficiente para permitir o paralelismo. Mas é o fato de programar/agendar a execução dessas sub-tarefas que realmente cria o paralelismo.

O ASyncIO faz exatamente isso, você pode estruturar o seu código em sub-tarefas definidas como coroutines e isso te permite programar a execução da maneira que desejar, incluindo a forma simultânea. As Corountines contém pontos de vazão demarcados com a palavra yield onde definimos onde uma troca de contexto poderia ocorrer caso existam outras tarefas pendentes, mas que não irá ocorrer caso não existam outras tarefas.

Nota do tradutor: Em uma loja de doces há um funcionário empacotando balas, ao finalizar cada pacote ele o lacra e coloca na vitrine (YIELD), então ele dá uma olhada no balcão para ver se tem algum cliente para ser atendido, se tiver um cliente, então ele para de empacotar balas atende o pedido do cliente (troca de contexto). E só depois de terminar de > atender o cliente ele então volta a empacotar as balas, caso não tenha cliente a ser atendido ele simplesmente continua o trabalho de empacotamento. Podemos dizer que é um funcionário fazendo duas tarefas __. (responda nos comentários se é paralelamente ou concorrentemente)

Uma troca de contexto no asyncio representa o event loop passando o fluxo
de controle da coroutine em execução para a próxima na fila de execução, ou seja, (Yielding).

Veja um exemplo básico:

import asyncio

@asyncio.coroutine
def empacotar_bala():
    print("Empacotando balas...")

    # parada para verificar se tem cliente no balcão
    yield from asyncio.sleep(0)

    # troca de contexto
    print("Explicitamente voltando a empacotar balas")


@asyncio.coroutine
def atender_balcao():
    print("Explicitamente verificando se tem cliente no balcão...")

    yield from asyncio.sleep(0)

    print("Voltando a empacotar as balas")


ioloop = asyncio.get_event_loop()  # Event Loop

tasks = [ioloop.create_task(empacotar_bala()),
         ioloop.create_task(atender_balcao())]

wait_tasks = asyncio.wait(tasks)

ioloop.run_until_complete(wait_tasks)

ioloop.close()

Execute:

$ python3 async1.py
Empacotando balas...
Explicitamente verificando se tem cliente no balcão...
Explicitamente voltando a empacotar balas
Voltando a empacotar as balas
  • Primeiramente nós declaramos duas tarefas simples com a intenção de serem executadas de maneira não bloqueante pois usamos a função sleep do asyncio.
  • Coroutines só podem ser chamadas por outras coroutines ou podem ser agrupadas em uma task para então serem enfileiradas, nós usamos a função create_task para fazer isso.
  • Então criamos lista contendo as 2 tasks e nós a combinamos em uma wait que é uma task que irá aguardar até que todas as tarefas
    enfileiradas terminem.
  • E finalmente nós programamos a wait para executar usando o event loop usando a função run_until_complete.

Ao usar yield from na coroutine empacotar_bala nós declaramos que a coroutine pode naquele momento passar o controle do fluxo de execução de volta para o event loop, neste caso o sleep ao terminar (note o sleep(0)) irá devolver o controle ao event loop que irá mudar de contexto, passando o controle de fluxo para a próxima coroutine agendada para execução: atender_balcao

Nota: O tradutor alterou os nomes das funções dos exemplos do artigo original para dar um significado mais fácil de ser interpretado em português mas mantendo a semântica e fluxo de execução dos códigos, todavia os originais estão no github.

Vamos agora simular duas tarefas bloqueantes gr1 e gr2, considere que há dois requests para serviços externos. Enquanto elas executam, uma terceira tarefa pode ser executada assíncronamente, como no seguinte exemplo:

import time
import asyncio

start = time.time()

def tic():
  return 'at %1.1f segundos' % (time.time() - start)


@asyncio.coroutine
def gr1():
  # Demora a ser executada, mas não queremos esperar
  print('gr1 iniciou a execução: {}'.format(tic()))
  yield from asyncio.sleep(2)
  print('gr1 terminou a execução: {}'.format(tic()))


@asyncio.coroutine
def gr2():
  # Demora a ser executada, mas não queremos esperar
  print('gr2 iniciou a execução: {}'.format(tic()))
  yield from asyncio.sleep(2)
  print('gr2 terminou a execução: {}'.format(tic()))


@asyncio.coroutine
def gr3():
  print('Executando enquanto as outras estão bloqueadas: {}'.format(tic()))
  yield from asyncio.sleep(5)
  print('Pronto!')

ioloop = asyncio.get_event_loop()
tasks = [
    ioloop.create_task(gr1()),
    ioloop.create_task(gr2()),
    ioloop.create_task(gr3())
]
ioloop.run_until_complete(asyncio.wait(tasks))
ioloop.close()

Execute:

$ python3 async2.py 
gr1 iniciou a execução: at 0.0 segundos
gr2 iniciou a execução: at 0.0 segundos
Executando enquanto as outras estão bloqueadas: at 0.0 segundos
gr1 terminou a execução: at 2.0 segundos
gr2 terminou a execução: at 2.0 segundos
Pronto!

Perceba que na forma que o I/O loop faz o gerenciamento e programa a execução permite que o seu código, rodando em single thread possa operar de forma concorrente. Enquanto duas tarefas estavam bloqueadas uma terceira pode tomar o controle do fluxo de execução e ser executada de maneira assíncrona.

Ordem de execução

No mundo síncrono estamos acostumados a pensar de maneira linear. Se nós tivermos uma lista de tarefas que consomem diferente quantidade de tempo elas serão executadas na ordem em que foram chamadas.

Porém, quando usamos concorrência nós precisamos estar cientes de que as tarefas terminam em tempos que diferem da ordem em que foram enfileiradas.

import random
from time import sleep
import asyncio


def task(pid):
    """Uma tarefa não deterministica"""
    sleep(random.randint(0, 2) * 0.001)
    print('Task %s terminada' % pid)


@asyncio.coroutine
def task_coro(pid):
    """Uma tarefa deterministica"""
    yield from asyncio.sleep(random.randint(0, 2) * 0.001)
    print('Task %s terminada' % pid)


def synchronous():
    for i in range(1, 10):
        task(i)


@asyncio.coroutine
def asynchronous():
    tasks = [asyncio.async(task_coro(i)) for i in range(1, 10)]
    yield from asyncio.wait(tasks)


print('Síncronamente:')
synchronous()

ioloop = asyncio.get_event_loop()
print('Assíncronamente:')
ioloop.run_until_complete(asynchronous())

ioloop.close()

Execute:

$ python3 async3.py 
Síncronamente:
Task 1 terminada
Task 2 terminada
Task 3 terminada
Task 4 terminada
Task 5 terminada
Task 6 terminada
Task 7 terminada
Task 8 terminada
Task 9 terminada

Assíncronamente:
Task 2 terminada
Task 4 terminada
Task 8 terminada
Task 5 terminada
Task 6 terminada
Task 7 terminada
Task 9 terminada
Task 1 terminada
Task 3 terminada

A saida será com certeza variada, pois cada task espera por uma quantidade randômica de tempo, mas repare que a ordem dos resultados é completamente diferente, mesmo tendo enfileirado em uma lista de tarefas na mesma ordem usando o mesmo range.

Outro detalhe é que tivemos que uma versão em coroutine da nossa simples função. É importante entender que o asyncio não faz com que as coisas se transformarem magicamente em não bloqueantes.

O AsyncIO está por enquanto sozinho na biblioteca padrão do Python 3 enquanto todos outros módulos oferecem apenas funcionalidades bloqueantes.

Você pode usar o módulo concurrent.futures para agrupar tarefas bloqueantes em uma thread ou um processo e então retornar um Future que o asyncio pode utilizar. Os mesmos exemplos utilizando threads podem ser encontrados no github

Esta é provavelmente a maior desvantagem ao usar asyncio neste momento, porém existe uma série de bibliotecas para diferentes tarefas e serviços que já estão disponíveis de maneira não bloqueante.


Uma tarefa bloqueante bastante comum é coletar dados de um serviço HTTP. Para isso vou usar a excelente biblioteca aiohttp que efetua chamadas não bloqueantes a serviços HTTP. Neste exemplo vamos coletar dados da API pública do Github e ler apenas o valor de Date do responde header.

import time
import urllib.request
import asyncio
import aiohttp

URL = 'https://api.github.com/events'
MAX_CLIENTS = 3


def fetch_sync(pid):
    print('Captura síncrona {} iniciou'.format(pid))
    start = time.time()
    response = urllib.request.urlopen(URL)
    datetime = response.getheader('Date')

    print('Processo {}: {}, demorou: {:.2f} segundos'.format(
        pid, datetime, time.time() - start))

    return datetime


@asyncio.coroutine
def fetch_async(pid):
    print('Captura assíncrona {} iniciou'.format(pid))
    start = time.time()
    response = yield from aiohttp.request('GET', URL)
    datetime = response.headers.get('Date')

    print('Processo {}: {}, demorou: {:.2f} segundos'.format(
        pid, datetime, time.time() - start))

    response.close()
    return datetime


def synchronous():
    start = time.time()
    for i in range(1, MAX_CLIENTS + 1):
        fetch_sync(i)
    print("Processo demorou: {:.2f} segundos".format(time.time() - start))


@asyncio.coroutine
def asynchronous():
    start = time.time()
    tasks = [asyncio.ensure_future(
        fetch_async(i)) for i in range(1, MAX_CLIENTS + 1)]
    yield from asyncio.wait(tasks)
    print("Processo demorou: {:.2f} segundos".format(time.time() - start))


print('Sincrono:')
synchronous()

print('Assíncrono:')
ioloop = asyncio.get_event_loop()
ioloop.run_until_complete(asynchronous())
ioloop.close()

Execute:

$ python3 -V
Python 3.4.4+

$ pip3 install aiohttp

$ python3 async4.py
Sincrono:
Processo sincrono 1 iniciou
Processo 1: Wed, 17 Feb 2016 13:10:11 GMT, demorou: 0.54 segundos
Processo sincrono 2 iniciou
Processo 2: Wed, 17 Feb 2016 13:10:11 GMT, demorou: 0.50 segundos
Processo sincrono 3 iniciou
Processo 3: Wed, 17 Feb 2016 13:10:12 GMT, demorou: 0.48 segundos
Process demorou: 1.54 segundos

Assíncrono:
Processo assincrono 1 iniciou
Processo assincrono 2 iniciou
Processo assincrono 3 iniciou
Processo 3: Wed, 17 Feb 2016 13:10:12 GMT, demorou: 0.50 segundos
Processo 2: Wed, 17 Feb 2016 13:10:12 GMT, demorou: 0.52 segundos
Processo 1: Wed, 17 Feb 2016 13:10:12 GMT, demorou: 0.54 segundos
Processo demorou: 0.54 segundos

Nota: requer Python 3.4.4+ caso contrário cairá em exception

Primeiramente, repare na diferença de tempo, usando chamadas assíncronas nós efetuamos as requisições ao serviço HTTP exatamente ao mesmo tempo (13:10:12). Como falado anteriormente, cada requisição passou (yield) o fluxo de controle para a próxima e retornou quando foi completada.

Resultando no fato de que requisitar e capturar os resultados de todos as tarefas demorou o mesmo tempo que a requisição mais lenta! Veja o tempo no log 0.54 segundos para a requisição mais lenta (processo 1) e é exatamente o mesmo tempo que se passou para processar todos os 3 requests, Legal né? (enquanto a parte de I/O daquela tarefa lenta estava bloqueada, as outras puderam ser executadas simultaneamente).

Agora veja como o código é similar ao da versão síncrona! é praticamente o mesmo código! As diferenças principais são por conta das diferenças de implementações das bibliotecas usadas e a parte da criação das tasks e a espera para elas terminarem.

Criando concorrência

Até então estamos usando uma única abordagem de criar e requisitar resultados de coroutines, criar uma lista de tasks e esperar que elas terminem.

Mas as coroutines podem ser programadas para serem executadas e requisitar resultados em maneiras diferentes. Imagine um cenário onde precisamos processar os resultados de uma chamada HTTP GET assim que ela é requisitada, o processo é na verdade similar ao que fizemos no exemplo anterior.

import time
import random
import asyncio
import aiohttp

URL = 'https://api.github.com/events'
MAX_CLIENTS = 3


@asyncio.coroutine
def fetch_async(pid):
    start = time.time()
    sleepy_time = random.randint(2, 5)
    print('Processo assincrono {} iniciou, esperando por {} segundos'.format(
        pid, sleepy_time))

    yield from asyncio.sleep(sleepy_time)

    response = yield from aiohttp.request('GET', URL)
    datetime = response.headers.get('Date')

    response.close()
    return 'Processo {}: {}, demorou: {:.2f} segundos'.format(
        pid, datetime, time.time() - start)


@asyncio.coroutine
def asynchronous():
    start = time.time()
    futures = [fetch_async(i) for i in range(1, MAX_CLIENTS + 1)]
    for i, future in enumerate(asyncio.as_completed(futures)):
        result = yield from future
        print('{} {}'.format(">>" * (i + 1), result))

    print("Processo demorou: {:.2f} segundos".format(time.time() - start))


ioloop = asyncio.get_event_loop()
ioloop.run_until_complete(asynchronous())
ioloop.close()

Execute

$ python3 async5.py

Processo assincrono 1 iniciou, esperando por 4 segundos
Processo assincrono 3 iniciou, esperando por 5 segundos
Processo assincrono 2 iniciou, esperando por 3 segundos
>> Processo 2: Wed, 17 Feb 2016 13:55:19 GMT, demorou: 3.53 segundos
>>>> Processo 1: Wed, 17 Feb 2016 13:55:20 GMT, demorou: 4.49 segundos
>>>>>> Processo 3: Wed, 17 Feb 2016 13:55:21 GMT, demorou: 5.48 segundos
Processo demorou: 5.48 segundos

Repare no deslocamento >> e no tempo de cada chamada, elas foram programadas ao mesmo tempo, os resultados chegam fora de ordem e são processados assim que chegam.

Este código é um pouco diferente, estamos agrupando as coroutines em uma lista, cada uma para ser agendada e executada. a função as_completed retorna um iterador que irá gerar (YIELD) um future completo assim que a tarefa estiver terminada. Muito legal né? aliás, as funções as_completed e wait são ambas originalmente parte do modulo concurrent.futures


Vamos pegar um outro exemplo, imagine que você está tentando consultar o seu endereço de IP público atual em seu programa. Existem alguns serviços que fornecem essa informação mas você não tem certeza se estarão acessíveis no momento da execução. Você não quer checar cada um deles sequencialmente, então, é preferível efetuar requisições concorrentes para cada um dos serviços e utilizar aquele que responder mais rapidamente, ok? Ok!

Bom, acontece que nosso velho amigo wait recebe o parâmetro return_when para fazer exatamente isso. Estávamos ignorando o dado retornado pelo wait já que estávamos preocupados apenas em paralelizar as tarefas. Mas agora nós queremos pegar os resultados da coroutine, então usaremos dois conjuntos de futures, done e pending.

from collections import namedtuple
import time
import asyncio
from concurrent.futures import FIRST_COMPLETED
import aiohttp

Service = namedtuple('Service', ('name', 'url', 'ip_attr'))

SERVICES = (
    Service('ipify', 'https://api.ipify.org?format=json', 'ip'),
    Service('ip-api', 'http://ip-api.com/json', 'query')
)


@asyncio.coroutine
def fetch_ip(service):
    start = time.time()
    print('Fetching IP from {}'.format(service.name))

    response = yield from aiohttp.request('GET', service.url)
    json_response = yield from response.json()
    ip = json_response[service.ip_attr]

    response.close()
    return '{} terminou com resultado: {}, demorou: {:.2f} segundos'.format(
        service.name, ip, time.time() - start)


@asyncio.coroutine
def asynchronous():
    futures = [fetch_ip(service) for service in SERVICES]
    done, pending = yield from asyncio.wait(
        futures, return_when=FIRST_COMPLETED)
    print(done.pop().result())


ioloop = asyncio.get_event_loop()
ioloop.run_until_complete(asynchronous())
ioloop.close()

Execute:

$ python3 async6.py
Fetching IP from ip-api
Fetching IP from ipify
ip-api terminou com resultado: 82.34.76.170, demorou: 0.09 segundos
Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x10f95c6d8>
Task was destroyed but it is pending!
task: <Task pending coro=<fetch_ip() running at 2c-fetch-first-ip-address-response.py:20> wait_for=<Future pending cb=[BaseSelectorEventLoop._sock_connect_done(10)(), Task._wakeup()]>>

Espere, o que aconteceu aqui? O primeiro serviço respondeu com sucesso então o que são esses warnings?

Bem, nós agendamos duas tarefas mas não permitimos que as duas fossem completadas. o AsyncIO considera que é um bug e imprime um warning. Então queremos informar ao event loop que este era um comportamento esperado e não se preocupar com a segunda tarefa que ficou pendente. Como? que bom que perguntou.

Estados do Futuro

(sobre o estado que um future está atualmente, não o estado que ele estará no futuro... você entendeu né!)

Eles são:

  • Pending
  • Running
  • Done
  • Cancelled

Simples assim, quando um future finaliza sua tarefa, esta tarefa irá retornar o resultado de volta para o future, se estiver em pending ou cancelled ele gera o erro InvalidStateError ou CancelledError, e finalmente se a coroutine gera o erro ele será re-gerado, o que significa o mesmo comportamento ao chamar exception. confira aqui

Você também pode usar .done, .cancelled e .running em uma Future para obter um booleano indicando o estado. Note que done significa simplesmente que o result irá retornar ou gerar um erro. Você pode explicitamente cancelar um Future chamando o método cancel, e isso é o que precisamos para resolver o warning anterior.



def asynchronous():
    futures = [fetch_ip(service) for service in SERVICES]
    done, pending = yield from asyncio.wait(
        futures, return_when=FIRST_COMPLETED)
    print(done.pop().result())

    for future in pending:
        future.cancel()

e então

$ python3 async6.py
Fetching IP from ip-api
Fetching IP from ipify
ip-api terminou com resultado: 82.34.76.170, demorou: 0.09 segundos

Um bom resultado né!

Futures permitem injetar callbacks a serem executados quando entrarem no estado done caso você queira adicionar uma lógica adicional, ou se você não quiser usar o yield from e prefira o callback hell, (sério quer mesmo?)

E você pode, para objetivos de unit-testing manualmente injetar o resultado ou uma exception a um Future.

Gerenciamento de erros

O AsyncIO é sobre fazer código concorrente gerenciável e legível, e isto se torna óbio no que diz respeito ao tratamento de erros. Vamos voltar ao exemplo anterior para ilustrar isso.

Imagine que queremos garantir que os 2 serviços retornaram o mesmo resultado, mas um dos serviços fica offline e não responde, podemos usar apenas o usual try... except

from collections import namedtuple
import time
import asyncio
from concurrent.futures import FIRST_COMPLETED
import aiohttp

Service = namedtuple('Service', ('name', 'url', 'ip_attr'))

SERVICES = (
    Service('ipify', 'https://api.ipify.org?format=json', 'ip'),
    Service('ip-api', 'http://ip-api.com/json', 'query'),
    Service('broken', 'http://este-servico-nao-funciona', 'ip')
)


@asyncio.coroutine
def fetch_ip(service):
    start = time.time()
    print('Fetching IP from {}'.format(service.name))

    try:
        response = yield from aiohttp.request('GET', service.url)
    except:
        return "{} não está respondendo".format(service.name)

    json_response = yield from response.json()
    ip = json_response[service.ip_attr]

    response.close()
    return '{} terminou com resultado: {}, demorou: {:.2f} segundos'.format(
        service.name, ip, time.time() - start)


@asyncio.coroutine
def asynchronous():
    futures = [fetch_ip(service) for service in SERVICES]
    done, _ = yield from asyncio.wait(futures)

    for future in done:
        print(future.result())


ioloop = asyncio.get_event_loop()
ioloop.run_until_complete(asynchronous())
ioloop.close()

execute:

$ python3 async7.py
Fetching IP from ip-api
Fetching IP from borken
Fetching IP from ipify
ip-api terminou com o resultado: 85.133.69.250, demorou: 0.75 segundos
ipify terminou com o resultado: 85.133.69.250, demorou: 1.37 segundos
borken não está respondendo

Também podemos tratar os erros enquanto processamos os resultados dos Futures em caso de algo não esperado acontecer (lembra que eu disse que os erros são re-gerados na coroutine).

from collections import namedtuple
import time
import asyncio
from concurrent.futures import FIRST_COMPLETED
import aiohttp
import traceback

Service = namedtuple('Service', ('name', 'url', 'ip_attr'))

SERVICES = (
    Service('ipify', 'https://api.ipify.org?format=json', 'ip'),
    Service('ip-api', 'http://ip-api.com/json', 'query'),
    Service('broken', 'http://este-servico-nao-funciona', 'ip')
)


@asyncio.coroutine
def fetch_ip(service):
    start = time.time()
    print('Fetching IP from {}'.format(service.name))

    try:
        response = yield from aiohttp.request('GET', service.url)
    except:
        return "{} não está respondendo".format(service.name)

    json_response = yield from response.json()
    ip = json_response[service.ip_attr]

    response.close()
    return '{} terminou com resultado: {}, demorou: {:.2f} segundos'.format(
        service.name, ip, time.time() - start)


@asyncio.coroutine
def asynchronous():
    futures = [fetch_ip(service) for service in SERVICES]
    done, _ = yield from asyncio.wait(futures)

    for future in done:
        try:
            print(future.result())
        except:
            print("Erro não esperado: {}".format(traceback.format_exc()))


ioloop = asyncio.get_event_loop()
ioloop.run_until_complete(asynchronous())
ioloop.close()

Veja:

$ python3 async7.py
Fetching IP from ipify
Fetching IP from borken
Fetching IP from ip-api
ipify terminou com o resultado: 85.133.69.250, demorou: 0.91 segundos
borken não está respondendo
Erro não esperado: Traceback (most recent call last):
 File “3b-fetch-ip-addresses-future-exceptions.py”, line 41, in asynchronous
 print(future.result())
 File “/usr/local/Cellar/python3/3.5.0/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/futures.py”, line 274, in result
 raise self._exception
 File “/usr/local/Cellar/python3/3.5.0/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/tasks.py”, line 239, in _step
 result = coro.send(value)
 File “3b-fetch-ip-addresses-future-exceptions.py”, line 27, in fetch_ip
 ip = json_response[service.ip_attr]
KeyError: ‘this-is-not-an-attr’

Da mesma forma que agendar uma task e não esperar que ela termine é considerado um bug, agendar uma task e não recuperar possíveis erros também irá gerar um warning.

from collections import namedtuple
import time
import asyncio
import aiohttp

Service = namedtuple('Service', ('name', 'url', 'ip_attr'))

SERVICES = (
    Service('ipify', 'https://api.ipify.org?format=json', 'ip'),
    Service('ip-api', 'http://ip-api.com/json', 'this-is-not-an-attr'),
    Service('borken', 'http://no-way-this-is-going-to-work.com/json', 'ip')
)


@asyncio.coroutine
def fetch_ip(service):
    start = time.time()
    print('Fetching IP from {}'.format(service.name))

    try:
        response = yield from aiohttp.request('GET', service.url)
    except:
        print('{} is unresponsive'.format(service.name))
    else:
        json_response = yield from response.json()
        ip = json_response[service.ip_attr]

        response.close()
        print('{} finished with result: {}, took: {:.2f} seconds'.format(
            service.name, ip, time.time() - start))


@asyncio.coroutine
def asynchronous():
    futures = [fetch_ip(service) for service in SERVICES]
    yield from asyncio.wait(futures)  # intentionally ignore results


ioloop = asyncio.get_event_loop()
ioloop.run_until_complete(asynchronous())
ioloop.close()

execute:

$ python3 async8.py
Fetching IP from ipify
Fetching IP from borken
Fetching IP from ip-api
borken is unresponsive
ipify finished with result: 85.133.69.250, took: 0.78 seconds
Task exception was never retrieved
future: <Task finished coro=<fetch_ip() done, defined at 3c-fetch-ip-addresses-ignore-exceptions.py:15> exception=KeyError(‘this-is-not-an-attr’,)>
Traceback (most recent call last):
 File “/usr/local/Cellar/python3/3.5.0/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/tasks.py”, line 239, in _step
 result = coro.send(value)
 File “3c-fetch-ip-addresses-ignore-exceptions.py”, line 26, in fetch_ip
 ip = json_response[service.ip_attr]
KeyError: ‘this-is-not-an-attr’

Se parece muito com a saída do exemplo anterior, mas não contém os mínimos detalhes e mensagens do asyncio.

Timeouts

E se nós não nos importarmos muito com o nosso IP? Imagine que é apenas um adicional ao nosso serviço, mas não importante, não queremos que o usuário fique esperando por este dado. Idealmente nós definimos um time-out para nossas tarefas não bloqueantes, e então continuamos nosso programa sem o atributo do IP já que neste exemplo não é tão importante.

De novo descobrimos que wait tem o atributo que precisamos:

import time
import random
import asyncio
import aiohttp
import argparse
from collections import namedtuple
from concurrent.futures import FIRST_COMPLETED

Service = namedtuple('Service', ('name', 'url', 'ip_attr'))

SERVICES = (
    Service('ipify', 'https://api.ipify.org?format=json', 'ip'),
    Service('ip-api', 'http://ip-api.com/json', 'query'),
)

DEFAULT_TIMEOUT = 0.01


@asyncio.coroutine
def fetch_ip(service):
    start = time.time()
    print('Fetching IP from {}'.format(service.name))

    yield from asyncio.sleep(random.randint(1, 3) * 0.1)
    try:
        response = yield from aiohttp.request('GET', service.url)
    except:
        return '{} is unresponsive'.format(service.name)

    json_response = yield from response.json()
    ip = json_response[service.ip_attr]

    response.close()
    print('{} finished with result: {}, took: {:.2f} seconds'.format(
        service.name, ip, time.time() - start))
    return ip


@asyncio.coroutine
def asynchronous(timeout):
    response = {
        "message": "Result from asynchronous.",
        "ip": "not available"
    }

    futures = [fetch_ip(service) for service in SERVICES]
    done, pending = yield from asyncio.wait(
        futures, timeout=timeout, return_when=FIRST_COMPLETED)

    for future in pending:
        future.cancel()

    for future in done:
        response["ip"] = future.result()

    print(response)


parser = argparse.ArgumentParser()
parser.add_argument(
    '-t', '--timeout',
    help='Timeout to use, defaults to {}'.format(DEFAULT_TIMEOUT),
    default=DEFAULT_TIMEOUT, type=float)
args = parser.parse_args()

print("Using a {} timeout".format(args.timeout))
ioloop = asyncio.get_event_loop()
ioloop.run_until_complete(asynchronous(args.timeout))
ioloop.close()

Repare no argumento timeout, também adicionamos um parâmetro de linha de comando para testar mais facilmente o que acontecerá se deixarmos a requisição ocorrer algumas vezes. Também adicionei um tempo randomico de slepp só para garantir que as coisas não aconteçam tão rápido que a gente nem perceba.

$ python async8.py

Using a 0.01 timeout
Fetching IP from ipify
Fetching IP from ip-api
{‘message’: ‘Result from asynchronous.’, ‘ip’: ‘not available’}

$ python async8.py -t 5
Using a 5.0 timeout
Fetching IP from ip-api
Fetching IP from ipify
ipify finished with result: 82.34.76.170, took: 1.24 seconds
{'ip': '82.34.76.170', 'message': 'Result from asynchronous.'}

Conclusão

Asyncio aumentou meu já enorme amor por Python. Para ser absolutamente honesto eu me apaixonei pelas coroutines em Python quando conheci o Tornado mas o asyncio conseguiu unir o melhor desta abordagem junto com excelentes bibliotecas de concorrência. E muito foi feito para que outras bibliotecas possam usar o IO loop, então se você está usando o Tornado, você também pode usa-lo com bibliotecas feitas para o asyncio!

E como eu disse anteriormente o maior problema por enquanto é a falta de bibliotecas e módulos que implementam o comportamento não bloqueante. Você pode achar que uma vasta quantidade de tecnologias já estabelecidas ainda não tenham uma versão não bloqueante para interagir com asyncio, ou que as existentes ainda estão jovens ou experimentais. Porém, O numero de bibliotexas está crescendo diariamente

confira este repo: https://github.com/aio-libs

Espero neste tutorial ter passado a idéia de como é prazeroso trabalhar com AsyncIO. Eu honestamente penso que isto é a peça que finalmente nos levará a adoção em massa e adaptação ao Python 3, essas são as coisas que você está perdendo se ficar parado no Python 2.7.

Uma coisa é certa, o Futuro do Python mudou completamente! (trocadilho intencional)

Castálio Podcast Especial Python Brasil Parte 3

Python Brasil parte 3

Fechando a série sobre a Python Brasil 12, neste episódio Eu, Og e Elyézer falamos sobre assuntos abordados em algumas palestras da conferência, comunidade, cervejas e Oktoberfest!

Como ouvir?

Acesse a seguinte URL http://castalio.info/episodio-75-python-brasil-12-parte-3.html para ouvir online e você também pode baixar os arquivos em MP3 ou Ogg.

Acompanhe!

Participe!

Se você tem sugestões, dicas de pessoas para serem entrevistadas, pautas a serem abordadas por favor entre em contato em um dos canais acima ou deixe comentários nos episódios.

Castálio Podcast Especial Python Brasil

Castálio?

Castália é o nome de uma náiade (uma ninfa aquática) que foi transformada por Apolo em nascente de água, perto de Delfos (a Fonte de Castália) e na base do Monte Parnaso.

Castália inspirava o génio poético daqueles que bebessem das suas águas ou ouvissem o movimento das suas águas. A água sagrada também era usada para as limpezas dos templos em Delfos.

Com o objetivo de entrevistar e ao mesmo tempo apresentar pessoas e projetos de tecnologia que sejam fonte de inspiração para os ouvintes, este podcast traz peridicamente uma nova vítima, err figura da comunidade de tecnologia que será sabatinada de todos os ângulos para o seu deleite!

Castálio na Python Brasil 12

Durante a Python Brasil 12 em Florianópolis eu e o Elyézer gravamos algumas entrevistas para o Castálio Podcast e com isso surgiu o convite do Og Maciel para eu integrar a equipe do Castálio, publicaremos 3 episódios especiais sore a Python Brasil 12, dois episódios com entrevistas e um terceiro com detalhes sobre nossa participação e o que fizemos por lá.


Lightning Cast

A idéia surgiu de ultima hora, em uma conversa de IRC durante o trabalho pensamos que seria interessante gravar pequenas entrevistas e ai surgiu o conceito de Lightning Cast! Paramos algumas pessoas no corredor da PyBR e convidamos para falar durante +-5 minutos.

Como foi a primeira vez que fizemos e bastante improvisado conseguimos fazer apenas 4 entrevistas, mas já nos serviu para validar a idéia e agora este novo formato fará parte do Castálio sempre que estivermos em eventos e conferências.

Entrevistas

Entrevistamos na primeira parte o Mário Sérgio que foi o Big Kahuna (organizador) da conferência e também a Naomi Cedar que foi Keynote do evento e faz parte da Python Software Foundation além de ser porta voz da questão de diversidade e inclusão na comunidade Python (esta segunda entrevista foi feita em Inglês).

Na segunda parte falamos com o pessoal do Projeto Serenata de Amor e depois com o Turicas mas não vou dar spoiler, vocês terão que ouvir o episódio na próxima semana para saber mais sobre a conversa.

Na terceira parte que será publicada daqui 2 semanas, Eu, Elyézer e o Og iremos bater um papo sobre a Python Brasil, Palestras que assistimos, pessoas que conhecemos e cervejas que tomamos, além é claro que falar sobre Floripa e a OktoberFest que foi o evento de fechamento da conferência.

Como ouvir?

Acesse a seguinte URL http://castalio.info/episodio-73-python-brasil-12-parte-1.html para ouvir online e você também pode baixar os arquivos em MP3 ou Ogg.

Acompanhe!

Participe!

Se você tem sugestões, dicas de pessoas para serem entrevistadas, pautas a serem abordadas por favor entre em contato em um dos canais acima ou deixe comentários nos episódios.

Castálio Podcast Especial Python Brasil Parte 2

Entrevistas na Python Brasil parte 2

Na segunda parte falamos com o pessoal do Projeto Serenata de Amor e depois com o Turicas mas não vou dar spoiler, vocês terão que ouvir o episódio para saber mais sobre a conversa.

E na semana que vem tem a terceira parte!

Como ouvir?

Acesse a seguinte URL http://castalio.info/episodio-74-python-brasil-12-parte-2.html para ouvir online e você também pode baixar os arquivos em MP3 ou Ogg.

Acompanhe!

Participe!

Se você tem sugestões, dicas de pessoas para serem entrevistadas, pautas a serem abordadas por favor entre em contato em um dos canais acima ou deixe comentários nos episódios.