Git Product home page Git Product logo

rules_pyz's Introduction

UNMAINTAINED

Unfortunately we decided not to use Bazel, so these rules are effectively unmaintained. If someone would like to take ownership, please push a copy of the code somewhere, and I will be happy to link to it from here. Thanks for the interest!

Bazel Python Zip Rules

This package is an alternative to Bazel's built-in Python rules that work with existing Python packages from PyPI. Eventually the built-in rules or rules_python should replace this, once Google improves them to work with external packages. Until then, these rules work for us.

See the example project for a tiny demonstration.

We named this rules_pyz because originally it built a zip for every pyz_binary and pyz_test. We have since changed it so it only optionally builds a zip.

Using the rules

Add the following lines to your WORKSPACE:

# Load the dependencies required for the rules
git_repository(
    name = "com_bluecore_rules_pyz",
    commit = "eb2527d42664bc2dc4834ee54cb1bb94a1d08216",
    remote = "https://github.com/TriggerMail/rules_pyz.git",
)

load("@com_bluecore_rules_pyz//rules_python_zip:rules_python_zip.bzl", "pyz_repositories")

pyz_repositories()

To each BUILD file where you want to use the rules, add:

load(
    "@com_bluecore_rules_pyz//rules_python_zip:rules_python_zip.bzl",
    "pyz_binary",
    "pyz_library",
    "pyz_test",
)

Instead of py_* rules, use pyz_*. They should work the same way. One notable difference is instead of imports to change the import path, you need to use the pythonroot attribute, which only applies to the srcs and data of that rule, and not all transitive dependencies.

PyPI dependencies

If you want to import packages from PyPI, write a pip requirements.txt file, then:

  1. mkdir -p third_party/pypi
  2. mkdir wheels
  3. Add the following lines to third_party/pypi/BUILD:
    load(":pypi_rules.bzl", "pypi_libraries")
    pypi_libraries()
  4. Add the following lines to WORKSPACE:
    load("@com_bluecore_rules_pyz//pypi:pip.bzl", "pip_repositories")
    pip_repositories()
  5. Generate the dependencies using the tool:
    bazel build @com_bluecore_rules_pyz//pypi:pip_generate_wrapper
    bazel-bin/external/com_bluecore_rules_pyz/pypi/pip_generate_wrapper \
        -requirements requirements.txt \
        -outputDir third_party/pypi \
        -wheelURLPrefix http://example.com/ \
        -wheelDir wheels
  6. If this depends on any Python packages that don't publish wheels, you will need to copy the wheels directory to some server where they are publicly accessible, and set the -wheelURLPrefix argument to that URL. We use a [Google Cloud Storage bucket](https://cloud.google.com/storage/docs/ access-public-data) and copy the wheels with: gsutil -m rsync -a public-read wheels gs://public-bucket
  7. Add the following lines to WORKSPACE to load the generate requirements:
    load("//third_party/pypi:pypi_rules.bzl", "pypi_repositories")
    pypi_repositories()

Local Wheels

As an alternative to publishing built wheels, you can check them in to your repository. If you omit the wheelURLPrefix flag, pip_generate will generate references relative to your WORKSPACE.

Motivation and problems with existing rules

Bluecore is experimenting with using Bazel because it offers two potential advantages over our existing environment:

  1. Reproducible builds and tests between machines: Today, we use a set of virtualenvs. When someone adds or removes an external dependency, or moves code between "packages", we need to manually run some scripts to build the virtualenvs. This is error prone, and a frequent cause of developer support issues.
  2. Faster tests and CI by caching results.
  3. One tool for all languages: Today our code is primarily Python and JavaScript, with a small amount of Go. As we grow the team, the code base, and add more tools, it would be nice if there was a single way to build and test everything.

The existing rules have a number of issues which interfere with these goals. In particular, we need to be able to consume packages published on PyPI. The existing rules have the following problems:

The bazel_rules_pex rules work pretty well for these cases. However, pex is very slow when packaging targets that have large third-party dependencies, since it unzips and rezips everything. These rules started as an exploration to understand why Pex is so slow, and eventually morphed into the rules as they are today.

Implementation overview

A Python "executable" is a directory tree of Python files, with an "entry point" module that is invoked as __main__. We want to be able to define executables with different sets of dependencies that possibly conflict (e.g. executable foo may want example_module to be imported, but executable bar may need example_module to not exist, or be a different version). To do this, we build a directory tree containing all dependencies, and generate a __main__.py. This makes the directory executable by either running python executable_exedir or python executable_exedir/__main__.py. To make this more convenient, we also generate a shell script to invoke python with the correct arguments.

For example, if we have an executable named executable, which runs a script called executable.py that depends on somepackage.module, it will generate the following files:

executable          (generated shell script: runs executable_exedir)
executable_exedir
├── executable.py
├── __main__.py     (generated main script: runs executable.py)
└── somepackage
    ├── __init__.py
    └── module.py

The executable_exedir can be zipped into a zip file with a #! interpreter line, so it can be directly executed. This may introduce incompatibilities: many Python programs depend on reading resource files relative to their source files which fails when loaded from a zip. By default for maximum compatibility, the __main__.py will unzip everything into a temporary directory, execute that, then delete it at execute. You can set zip_safe=True on the pyz_binary to override this behaviour. At build time, if any native code libraries are detected, the rules write a manifest (_zip_info_.json) that instructs __main__.py to unpack these files because they cannot be loaded from a zip.

Creating an "isolated" Python environment

We want executables generated by these rules to be "isolated": They should only rely on the system Python interpreter and the standard library. Any custom packages or environment variables should be ignored, so running the program always works. It turns out this is tricky: If the default python binary is part of a virtual environment for example, in behaves slightly differently than a "normal" Python interpreter. Users may have added .pth files to their site-packages directory to customize their environment. To resolve this, __main__.py tries very hard to establish a "clean" environment, which complicates the startup code.

To do it, we execute __main__.py with the Python flags -E -S -s which ignores PYTHON* environment variables, and does not load site-packages. Unfortunately, to execute a zip, Python needs the runpy module which is in site-packages. Additionally, people might explicitly execute python ..._exedir or python ..._exedir/__main__.py. In those cases, if we find the site module, we re-execute Python with the correct flags, to ensure the program sees a clean environment.

TODO: pex has code to clean the sys.modules which we should borrow at some point, since it avoids re-executing Python which decreases startup overhead.

Bazel implementation: Runfiles

To make tests run quickly in Bazel, it is best to not copy files into an _exedir. Instead, we build the _exedir in the Bazel .runfiles directory using symlinks. This makes incremental changes much faster. As a disadvantage, it causes some slightly different paths than when things are packaged directly into a zip.

Unscientific comparison

  1. Modify a Python test file:
    • bazel_rules_pex: 5.252s total: 2.686s to package the pex; 2.533s to run the test
    • rules_pyz: ~2.278s to run the test (no rebuild needed: test srcs not packaged)
  2. Modify a lib file dependeded on by a Python test:
    • bazel_rules_pex: 5.276s total: 2.621s to package the pex; 2.624s to run the test
    • rules_pyz: 0.112s to pack the dependencies; 2.180s to run the test
  3. Package numpy and scipy with a main that imports scipy
    • pex: 9.5s ; time to run: first time: 1.3s; next times: 0.4s
    • simplepack: 0.6s; time to run: 0.5s

Notes

rules_pyz's People

Contributors

globegitter avatar joshclimacell avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

rules_pyz's Issues

Compatibility with rules_protobuf?

Has anyone gotten rules_pyz working with rules_protobuf's py_proto_library rule? I can't add the target as a dep because a provider mismatch:

ERROR: <snip>: in deps attribute of pyz_library rule <snip>:python_default_library: '<snip>:python_proto' does not have mandatory providers: 'PyZProvider'

multiple requirements.txt files

How does one set up a monorepo with multiple packages, each using their own requirements.txt?

Using one //third_party/pypi would be the best, but perhaps right now the easiest is to use //third_party/LOCALPACKAGE/pypi/... or //third_party/pypi/LOCALPACKAGE/...

difference between pyz_binary and pyz_image

I was trying to get a legacy app working with these rules (I have not written it myself, it uses Django's manage.py) and to get things working I had to add pythonroot="." to my python libraries (otherwise libraries could not be imported). But now running the app in docker via the pyz3_image complained that it could not import a library, specifically the DJANGO_SETTINGS_MODULE. Now I additionally added this library without the pythonroot settings and now it works.

I am sorry I can not give more details at this stage I thought I would just report it and if I have more time to investigate I can add more here. I am wondering though if there is a slight difference in the directory layout, or maybe where the process is being executed compared to the runfiles dir locally?

Does not have mandatory providers: 'PyZProvider'

Hi just trying out these rules and running into an issue with local wheels. I am getting

(14:30:51) ERROR: /somepath/third_party/pypi/py3/BUILD.bazel:2:1: in deps attribute of pyz_library rule //third_party/pypi/py3:ipaddress: '//third_party/pypi/py3:py3_ipaddress' does not have mandatory providers: 'PyZProvider'. Since this rule was created by the macro 'pypi_libraries', the error might have been caused by the macro implementation in /somepath/third_party/pypi/py3/rules.bzl:241:14
(14:30:52) ERROR: Analysis of target '@workspace//:manage' failed; build aborted: Analysis of target '//third_party/pypi/py3:ipaddress' failed; build aborted

This error makes sense as the local wheels are a filegroup but are added as a dependency to pyz_library - but not sure what the intendend behaviour is. I suppose there should be something extracting the wheels and adding a pyz_library for it no?

Hermetic python

One thing that is quite nice about bazel is that it provides hermeticity, i.e. it does not depend on your system tools (at least to some degree). As an example rules_go and rules_nodejs do that, on first invocation they download the sdk / nodejs and only that is used to build/run your apps. I think it is pretty nice because one does not have to worry about what they have installed on their system (or in the docker container) and bazel just provides the version needed and everyone is exactly on the same version. That in combination with the isolation should provide some pretty nice guarantees and also a nicer dev experience especially if it could support different interpreter versions in a project (at the very least py2/py3).

Is that something you have considered before?

pip_generate.go cannot be used

Looks like the pip_generate executable (osx) has a hardcoded path from a user home directory:

/bazel-bin/external/com_bluecore_rules_pyz/pypi/pip_generate_wrapper     -requirements requirements.txt     -outputDir third_party/pypi     -wheelURLPrefix http://example.com/     -wheelDir wheels
panic: open third_party/pypi/pypi_rules.bzl: no such file or directory

goroutine 1 [running]:
main.main()
	/Users/evanjones/t/rules_pyz/pypi/pip_generate.go:340 +0x4546

Why not use rules_go to build this directly?

pyz_binary is missing the PyZProvider

In order to write unit tests for pyz_binary content, my team wants to be able to add pyz_test rules that have deps on pyz_binary targets.

Unfortunately, that triggers an error because pyz_binary rules do not have the mandatory provider PyZProvider. That is also different behavior than the native rules where py_binary rules do have the py provider.

Simply updating this line to return [provider, DefaultInfo( seems to make it work as desired:
https://github.com/TriggerMail/rules_pyz/blob/master/rules_python_zip/rules_python_zip.bzl#L243

My question is, why is the provider omitted from pyz_binary rules? Can it be added?

generate wrapper should work as indicated in README.md

Thanks for this work.

The instructions in the README.md do not work correctly for a first time run.

Instruction #3 asks you to place load("//third_party/pypi:pypi_rules.bzl", "pypi_repositories") pypi_repositories() in your WORKSPACE.
Instruction #4 asks you to run bazel build @com_bluecore_rules_pyz//pypi:pip_generate_wrapper

This will error out since

bazel build @com_bluecore_rules_pyz//pypi:pip_generate_wrapper
bazel-bin/external/com_bluecore_rules_pyz/pypi/pip_generate_wrapper \
    -requirements requirements.txt \
    -output third_party/pypi/pypi_rules.bzl \
    -wheelURLPrefix http://example.com/

is what creates the pypi_rules.bzl in the first place.
This seems to be a "chicken and egg" situation. How do you handle this in a CI environment?

Thank you,
David

Only a small set of env. vars are passed on to the main script.

We rely on a environment variable 'ENV' (i.e. 'production' or 'staging') for our web services to know what environment they're running in. When dumping the OS environment variables in my pyz2_image's binary, it only prints four variables: PATH, HOSTNAME, HOME, and PYTHONPATH.

It looks like the remaining environment variables are stripped from the execution environment. Is there a way to allow certain env. variables to be carried along? Or is this a bug?

Support for PyPI dependencies that don't have precompiled wheels?

Hi!

Really interesting set of tools you've put together, and I'm eager to experiment with them.

One confusion for me is how to handle PyPI dependencies that don't get an automatic, correct http_file rule generated by pip_generate. I think this occurs when there isn't a precompiled .whl available in PyPI... E.g. if I just have docopt in my requirements.txt file, and try to follow the instructions in your README, I get the following in my generated third_party/pypi/pypi_rules.bzl:

def pypi_repositories():
    native.http_file(
        name="pypi_docopt",
        url="***ERROR***/docopt-0.6.2-py2.py3-none-any.whl",
        sha256="0b670b24f445201c64d506dd6df020215003b0855d242574b2f90ed17592aa89",
    )

where I have ***ERROR*** in the url attribute since I ran with -wheelURLPrefix '***ERROR***/'. What should I do in this case? I noticed the -wheelDir arg, but I'm not sure how to use it productively.

Any help would be appreciated!

Missing mandatory provider 'PyZProvider' for some packages.

Got the following error when trying to use the generated rules:

.../third_party/pypi/BUILD:2:1: in deps attribute of pyz_library rule //third_party/pypi:pyyaml: '//third_party/pypi:pypi_pyyaml' does not have mandatory providers: 'PyZProvider'. Since this rule was created by the macro 'pypi_libraries', the error might have been caused by the macro implementation in .../third_party/pypi/pypi_rules.bzl:87:14

Here are the relevant parts from the generated pypi_rules.bzl:

    pyz_library(
        name="itsdangerous",
        deps=[
        ] + ["@pypi_itsdangerous//:lib"],
        licenses=["notice"],
        visibility=["//visibility:public"],
    )
    if "pypi_itsdangerous" not in existing_rules:
        http_archive(
            name="pypi_itsdangerous",
            url="https://pypi.org/itsdangerous-0.24-cp27-none-any.whl",
            sha256="11dee8cc20825929c37f9686c16abd734ac774539aedad2de53a59287f4f5a54",
            build_file_content=_BUILD_FILE_CONTENT,
            type="zip",
        )

itsdangerous is a transitive dependency of flask:

    pyz_library(
        name="flask",
        deps=[
            "jinja2",
            "werkzeug",
            "click",
            "itsdangerous",
        ] + ["@pypi_flask//:lib"],
        licenses=["notice"],
        visibility=["//visibility:public"],
    )

This is how I ran the generator:

$ bazel-bin/external/com_bluecore_rules_pyz/pypi/pip_generate_wrapper -requirements requirements.txt     -outputDir third_party/pypi -wheelDir wheels
running pip to resolve dependencies ...
pip executed in 16.615124315s
wheeltool Flask-1.0.2-py2.py3-none-any.whl took 95.653503ms
wheeltool Flask_Cors-3.0.6-py2.py3-none-any.whl took 86.202392ms
wheeltool Flask_RESTful-0.3.6-py2.py3-none-any.whl took 82.261811ms
wheeltool Flask_Testing-0.7.1-cp27-none-any.whl took 86.673202ms
wheeltool Jinja2-2.10-py2.py3-none-any.whl took 87.465609ms
wheeltool Logentries-0.17-cp27-none-any.whl took 89.884904ms
wheeltool MarkupSafe-1.0-cp27-cp27mu-linux_x86_64.whl took 82.824924ms
wheeltool PyYAML-3.13-cp27-cp27mu-linux_x86_64.whl took 85.953027ms
wheeltool Werkzeug-0.14.1-py2.py3-none-any.whl took 90.921861ms
wheeltool aniso8601-3.0.2-py2.py3-none-any.whl took 86.62658ms
wheeltool arrow-0.12.1-py2.py3-none-any.whl took 87.37365ms
wheeltool backports.functools_lru_cache-1.5-py2.py3-none-any.whl took 84.239788ms
wheeltool bearfield-1.9.6-cp27-none-any.whl took 96.696084ms
wheeltool boto3-1.6.7-py2.py3-none-any.whl took 85.727373ms
wheeltool botocore-1.9.7-py2.py3-none-any.whl took 95.211566ms
wheeltool cassandra_driver-3.15.1-cp27-cp27mu-linux_x86_64.whl took 87.947339ms
wheeltool certifi-2018.8.24-py2.py3-none-any.whl took 82.744718ms
wheeltool chardet-3.0.4-py2.py3-none-any.whl took 86.263699ms
wheeltool click-6.7-py2.py3-none-any.whl took 91.391886ms
wheeltool clickclick-1.2.2-py2.py3-none-any.whl took 90.360049ms
wheeltool connexion-1.5.2-py2.py3-none-any.whl took 114.937781ms
wheeltool core_common-1.1.6.dev7-py2-none-any.whl took 82.621371ms
wheeltool core_lib_settings-2.1.1.dev6-py2-none-any.whl took 88.013349ms
wheeltool core_wsgi_common-1.1.9.dev39-py2-none-any.whl took 86.271815ms
wheeltool docutils-0.14-py2-none-any.whl took 90.591291ms
wheeltool era-1.1-cp27-none-any.whl took 83.612167ms
wheeltool functools32-3.2.3.post2-cp27-none-any.whl took 86.50944ms
wheeltool futures-3.2.0-py2-none-any.whl took 87.384391ms
wheeltool idna-2.7-py2.py3-none-any.whl took 87.128061ms
wheeltool inflection-0.3.1-cp27-none-any.whl took 85.831837ms
wheeltool itsdangerous-0.24-cp27-none-any.whl took 84.930344ms
wheeltool jmespath-0.9.3-py2.py3-none-any.whl took 85.070591ms
wheeltool jsonschema-2.6.0-py2.py3-none-any.whl took 87.642502ms
wheeltool lumberjack-0.0.1.dev58-py2-none-any.whl took 86.077702ms
wheeltool marshmallow-2.15.4-py2.py3-none-any.whl took 85.888258ms
wheeltool pathlib-1.0.1-cp27-none-any.whl took 86.853153ms
wheeltool pymongo-3.7.1-cp27-cp27mu-manylinux1_x86_64.whl took 95.765067ms
wheeltool pymongo-3.7.1-cp27-cp27m-macosx_10_13_intel.whl took 95.821527ms
wheeltool python_dateutil-2.7.3-py2.py3-none-any.whl took 82.408511ms
wheeltool python_json_logger-0.1.9-py2.py3-none-any.whl took 87.68203ms
wheeltool pytz-2018.5-py2.py3-none-any.whl took 90.02105ms
wheeltool requests-2.19.1-py2.py3-none-any.whl took 88.329245ms
wheeltool rollbar-0.14.4-cp27-none-any.whl took 81.104007ms
wheeltool s3transfer-0.1.13-py2.py3-none-any.whl took 91.52873ms
wheeltool setuptools-40.2.0-py2.py3-none-any.whl took 98.4868ms
wheeltool six-1.11.0-py2.py3-none-any.whl took 84.668683ms
wheeltool swagger_spec_validator-2.4.0-py2.py3-none-any.whl took 84.809537ms
wheeltool typing-3.6.6-py2-none-any.whl took 87.294736ms
wheeltool tzlocal-1.5.1-cp27-none-any.whl took 87.637275ms
wheeltool ujson-1.35-cp27-cp27mu-linux_x86_64.whl took 84.913026ms
wheeltool urllib3-1.23-py2.py3-none-any.whl took 91.884517ms

Python 3 rules do not correctly generate platform independent rules

Command line:

function update() {
     BASE_DIR="third_party/pypi"
     WHEELS_DIR_NAME="wheels"
     RULES_FILE_NAME="rules.bzl"

     reqs_file="$1"
     output_dir="$2"
     python_path="$3"

     bazel build @com_bluecore_rules_pyz//pypi:pip_generate_wrapper && \
     bazel-bin/external/com_bluecore_rules_pyz/pypi/pip_generate_wrapper \
         -requirements "$BASE_DIR/${reqs_file}" \
         -outputDir "$BASE_DIR/$output_dir" \
         -wheelDir "$WHEELS_DIR_NAME" \
         -wheelURLPrefix "https://storage.googleapis.com/dronedeploy-pypi-public/" \
         -outputBzlFileName "$RULES_FILE_NAME" \
         -pythonPath "$python_path" \
         -workspacePrefix "${output_dir}_"
 }

 update py2_requirements.txt py2 python2
 update py3_requirements.txt py3 python3

Python 2:

     pyz_library(
         name = "cryptography",
         deps = [
             "asn1crypto",
             "cffi",
             "enum34",
             "idna",
             "ipaddress",
             "six",
         ] + select({
             "@com_bluecore_rules_pyz//rules_python_zip:linux": ["@py2_cryptography__linux//:lib"],
             "@com_bluecore_rules_pyz//rules_python_zip:osx": ["@py2_cryptography__osx//:lib"],
         }),
         licenses = ["notice"],
         visibility = ["//visibility:public"],
     )

...

    if "py2_cryptography__linux" not in existing_rules:
         http_archive(
             name = "py2_cryptography__linux",
             url =
                 "https://files.pythonhosted.org/packages/87/e6/915a482dbfef98bbdce6be1e31825f591fc67038d4ee09864c1d2c3db371/cryptography-2.3.1-cp27-cp27mu-manylinux1_x86_64.   whl",
             sha256 =
                 "31db8febfc768e4b4bd826750a70c79c99ea423f4697d1dab764eb9f9f849519",
             build_file_content = _BUILD_FILE_CONTENT,
             type = "zip",
         )
     if "py2_cryptography__osx" not in existing_rules:
         http_archive(
             name = "py2_cryptography__osx",
             url =
                 "https://files.pythonhosted.org/packages/5d/b1/9863611b121ee524135bc0068533e6d238cc837337170e722224fe940e2d/cryptography-2.3.1-cp27-cp27m-macosx_10_6_intel.    whl",
             sha256 =
                 "17db09db9d7c5de130023657be42689d1a5f60502a14f6f745f6f65a6b8195c0",
             build_file_content = _BUILD_FILE_CONTENT,
             type = "zip",
         )

Python 3:

     pyz_library(
         name = "cryptography",
         deps = [
             "asn1crypto",
             "cffi",
             "enum34",
             "idna",
             "ipaddress",
             "six",
         ] + ["@py3_cryptography//:lib"],
         licenses = ["notice"],
         visibility = ["//visibility:public"],
     )

...

    if "py3_cryptography" not in existing_rules:
         http_archive(
             name = "py3_cryptography",
             url =
                 "https://files.pythonhosted.org/packages/98/0b/a6f293e5f10095dd8657a1b125c1ba6995c59d39cd8e20355475c8f760d0/cryptography-2.3.1-cp34-abi3-macosx_10_6_intel.     whl",
             sha256 =
                 "dc2d3f3b1548f4d11786616cf0f4415e25b0fbecb8a1d2cd8c07568f13fdde38",
             build_file_content = _BUILD_FILE_CONTENT,
             type = "zip",
         )

Weird way to use the provider in pyz_binary

Thanks a lot for the code, it's really useful!

I've noticed that pyz_binary uses the provider in a weird way. Usually, these leaf nodes just consume the transitive info, and can do:

for dep in ctx.attr.deps:
    for mapping in dep[PyZProvider].transitive_mappings:
        links[....

This is an updated reference for the canonical way to set this up.

Duplicate entries in deps

Somehow, from this line,

isort==4.3.4

one gets

    pyz_library(
        name="isort",
        deps=[
            "futures",
            "futures",
        ] + ["@pypi_isort//:lib"],
        licenses=["notice"],
        visibility=["//visibility:public"],
    )

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.