Giter VIP home page Giter VIP logo

robots-from-jupyter / robotframework-jupyterlibrary Goto Github PK

View Code? Open in Web Editor NEW
22.0 2.0 9.0 911 KB

A Robot Framework library for testing Jupyter end-user applications and extensions

Home Page: https://robotframework-jupyterlibrary.rtfd.io

License: BSD 3-Clause "New" or "Revised" License

RobotFramework 18.71% Python 80.81% Shell 0.48%
binder jupyter jupyterlab notebooks robotframework acceptance-testing rpa screenshots selenium

robotframework-jupyterlibrary's Introduction

robotframework-jupyterlibrary

A Robot Framework library for automating (testing of) Jupyter end-user applications and extensions

pip conda docs demo actions
pip-badge conda-forge-badge docs-badge binder-badge workflow-badge

Using

Write .robot files that use JupyterLibrary keywords... or use magics in notebooks.

*** Settings ***
Library           JupyterLibrary
Suite Setup       Wait For New Jupyter Server To Be Ready  jupyter-lab
Test Teardown     Reset JupyterLab And Close
Suite Teardown    Terminate All Jupyter Servers

*** Test Cases ***
A Notebook in JupyterLab
    Open JupyterLab
    Launch A New JupyterLab Document
    Add And Run JupyterLab Code Cell
    Wait Until JupyterLab Kernel Is Idle
    Capture Page Screenshot

See the acceptance tests for examples.

Installation

pip install robotframework-jupyterlibrary

Or

mamba install -c conda-forge robotframework-jupyterlibrary

Or (if you must):

conda install -c conda-forge robotframework-jupyterlibrary

Or see the contributing guide for a development install.

Free Software

JupyterLibrary is Free Software under the BSD-3-Clause License. It contains code from a number of other projects:

Some of its testing approaches (only distribtued in source form, not e.g. wheels) are also derived from other tools:

robotframework-jupyterlibrary's People

Contributors

azure-pipelines[bot] avatar bollwyvl avatar consideratio avatar jtpio avatar lugi0 avatar martinrenou avatar

Stargazers

 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

robotframework-jupyterlibrary's Issues

How to get elements

my company uses JupyterLab to provide example code as well as a terminal, I am trying to automate the running of these examples and enter commands in the terminal.
I have written a very simple test so far just opening the JupyterLab site and if I try to use inspect to get the elements it doesn't work mainly because I am in JupyterLab

Possible bug in `Maybe Accept a JupyterLab Prompt` keyword

This keyword checks for a jupyterlab modal using a css selector, and, if found, tries clicking a button (the first one).

The problem seems to be that the selector returns an element every time, even when there's no modal open in the JL UI. In fact, using the dev console, I get two hits with that selector all the time.

The keyword does work when there's a modal/prompt open, but it will also fail whenever it is called and no prompt is available.

I believe that changing the selector from the css based one to an xpath one would solve this issue. Specifically, reworking the keyword like this is currently working fine for me:

[Documentation]    Click the save button in a JupyterLab dialog (if one is open).
    ${accept} =    Get WebElements    xpath://div[contains(concat(' ',normalize-space(@class),' '),' jp-Dialog-footer ')]
    Run Keyword If    ${accept}    Click Element    ${accept[-1]}

Note that I also changed from accept[0] to accept[-1], since the last button is usually the confirmation one (e.g. saving a file when closing it has three buttons, Dismiss, Cancel and Save in that order).

If you agree that the behaviour of the keyword is currently not working as expected I can open a PR to merge my changes.

Keyword "Add and Run JupyterLab Code Cell" fails with ElementNotInteractableException when more than one file is open

Description of the problem:

When executing the keyword Add and Run JupyterLab Code Cell when multiple files are open in JupyterLab an ElementNotInteractableException will be thrown.

Clicking element 'xpath://div[contains(@class, 'jp-NotebookPanel-toolbar')]//*[@data-icon='ui-components:add']'
ElementNotInteractableException: Message: element not interactable
  (Session info: chrome=88.0.4324.150)

Expected behavior:

Add and Run JupyterLab Code Cell will add and run a new cell in notebook file tab that is open for editing

Possible Solution

When searching for the element //div[contains(@class, 'jp-NotebookPanel-toolbar')]//*[@data-icon='ui-components:add'] use the currently selected/visible notebook as the root

Package Versions:

JupyterLab v3.0.7
robotframework==4.0
robotframework-jupyterlibrary==0.3.0
robotframework-seleniumlibrary==5.1.3

Workaround:

Close all other files that are open in the NotebookPanel for browser

Get ReadTheDocs working again

Docs build is currently failing on PRs, probably would on merge to master.

Tasks:

  • CONDA_USES_MAMBA
    • requested from RTD support
  • resolve
  • rework .readthedocs.yml

0.3.0 Release Postmortem

  • #28 magic deps
    • The intent of the magic was not to make this a full interactive environment, but at present all the imports from ipython and pygments get resolved at import of JupyterLibrary.
    • this should be relaxed, and a "clean" development should be tested with e.g. conda-build.
  • #28 no release docs
    • should add a section to CONTRIBUTING
  • #28 no release env
    • twine is my go-to for "classic" packages, but has a lot of deps... probably needs a dedicated environment

Possibly broken when used with latest dependencies

I think for example Open JupyterLab is broken currently, because in jupyterhub/jupyter-server-proxy's test suite we have a failure that shows up even if jupyter-server-proxy hasn't become involved yet.

The test failing is declared here, and its slimmed down to try to isolate what the failure is about. I expect the failure to show in this projects test suite as well.

This is the information I see from the failure:

Set Up And More Test                                                  | FAIL |
WebDriverException: Message: Process unexpectedly closed with status 1

Support JupyterLab 4, Notebook 7

Elevator Pitch

Support any new quirks of JupyterLab 4 and Notebook 7. Drop support for JupyterLab 1 and 2. Maintain jquery notebook, for now.

Motivation

Testing migration to JupyterLab 4 and Notebook 7.

Design Ideas

  • ???

Release 0.5.0

`Open With JupyterLab Menu File, Log Out` fails with JavaScript exception in JupyterHub environment

As the title explains, running the Open With JupyterLab Menu keyword with File, Log Out arguments fails with the following JavaScript exception:

JavascriptException: Message: javascript error: Failed to execute 'elementsFromPoint' on 'Document': The provided double value is non-finite.

The issue seems to be that the locator used by the Click JupyterLab Menu Item keyword returns two hits when using Log Out as the argument in a JupyterHub environment (i.e. the hub:logout element is active instead of the filemenu:logout one).
In a single-user environment (i.e. the filemenu:logout element is active) the issue does not appear (because the hidden hub:logout element does not have the Log Out text).

There could be a number of ways to solve this issue, but before sending a PR I would like to get feedback on what would be the preferred solution. My current idea is to do a check on the ${label} passed to the keyword, and in the case of Log Out update the locator used to this:
//div[contains(@class, 'p-Menu-itemLabel')][text() = 'Log Out']/..[not(contains(@class,'p-mod-disabled'))]

The ..[not(contains(@class,'p-mod-disabled'))] part will filter out the inactive element out of the two, and will work in both environments.

This could also be used for all inputs, as it should not break any existing functionality and will handle any ${label} that happens to exist twice; I have however not tested this yet on such a large scale.

Using a custom command is awkward

In the case of robotlab, which doesn't call jupyter notebook, it's pretty awkward to hand off the arguments. Should figure out something a little more elegant. Simplest fix might be jupyter-notebook.

Release 0.4.1

  • merge all outstanding PRs
  • ensure the versions have been bumped (check with doit)
  • ensure the CHANGELOG is up-to-date
    • move the new release to the top of the stack
  • validate on binder
  • validate on ReadTheDocs
  • wait for a successful build of main
  • download the dist archive and unpack somewhere (maybe a fresh dist)
  • create a new release through the GitHub UI
    • paste in the relevant CHANGELOG entries
    • upload the artifacts
  • actually upload to npm.com, pypi.org
    cd dist
    twine upload *.tar.gz *.whl
  • postmortem

Support RobotFramework 6

Robotframework 6 is released, and should be included in the test matrix.

This would be an appropriate time to drop robotframework 3.x as well, as we're already autoformatting down to 4 anyway, and are likely just be trusting our existing tests.

Release 0.3.1

  • blockers
    • #38 fix docs build
    • #33 mystnb/pydata-sphinx-theme
    • #36 add some backwards-compatible options for interacting with multiple notebooks in Lab
    • #37 add some more timeout args
      • maybe add some more in some places where we hard-code them
  • infra
    • resolve envs
    • validate on binder
    • tag
    • upload
      • pypi
    • postmortem
      • conda-forge
      • update release process notes

Release 0.4.0

  • merge all outstanding PRs
  • start a release issue with a checklist (maybe like this one)
  • ensure VERSION has been increased appropriately
  • ensure the HISTORY.ipynb is up-to-date
  • validate on binder
  • validate on ReadTheDocs
  • wait for a successful build of master
  • download the dist archive and unpack somewhere (maybe a fresh dist)
  • create a new release through the GitHub UI
    • paste in the relevant HISTORY entries
    • upload the artifacts
  • actually upload to pypi.org
    doit publish
  • postmortem
    • handle conda-forge feedstock tasks
    • validate on binder via simplest-possible gists
    • activate the version on ReadTheDocs
    • bump to next development version
    • update release procedures

Investigate windows/ff failure

Might just need a different Wait... or Sleep, sigh.

2018-12-17T01:32:43.5588489Z headlessfirefox on windows.Lab.10 Notebook                                    
2018-12-17T01:32:43.5588917Z ==============================================================================
2018-12-17T01:32:54.5248058Z IPython Notebook on Lab                                               
| FAIL |
2018-12-17T01:32:54.5248320Z ElementClickInterceptedException: 
Message: Element <button 
  class="jp-ToolbarButtonComponent"> is not clickable at 
  point (383.2166748046875,73) because 
  another element <div class="p-Widget jp-Spinner"> obscures it

https://dev.azure.com/nickbollweg/nickbollweg/_build/results?buildId=145&view=logs&jobId=2d2b3007-3c5c-5840-9bb0-2b1ea49925f3&taskId=77aad734-2057-5694-9ae2-ee1f5f26eae8&lineStart=74&lineEnd=78&colStart=1&colEnd=1

Use myst/jupyter-book for docs

Consider replacing nbsphinx with jupyter-book (but probably myst-nb, because RTD)

  • seems to be more stable between versions

Deprecation warnings showing up `'[Return]' setting is deprecated`

I think the following warnings seen in https://github.com/jupyterhub/jupyter-server-proxy/actions/runs/7957862509/job/21721594362 is related to this project.

Here is an example where [Return] is used:

These deprecation warnings may have started to appear using jupyterlab-4.1.1 notebook-7.1.0 compared to jupyterlab-4.0.12 notebook 7.0.8, but I'm not confident on what provides the warning so I'm confused. Using jupyterhub/jupyter-server-proxy's test suite, they show up for the test_robot pytest when using notebook 7.1.0 but not when using notebook 7.0.8.

==============================================================================
Acceptance :: Acceptance tests for jupyter-server-proxy                       
==============================================================================
Acceptance.Lab :: Server Proxies in Lab                                       
==============================================================================
Lab Loads                                                             | PASS |
------------------------------------------------------------------------------
Launch Browser Tab                                                    | PASS |
------------------------------------------------------------------------------
Launch Lab Tab                                                        | PASS |
------------------------------------------------------------------------------
Acceptance.Lab :: Server Proxies in Lab                               | PASS |
3 tests, 3 passed, 0 failed
==============================================================================
Acceptance.Notebook :: Server Proxies in Notebook                             
==============================================================================
Notebook Loads                                                        | PASS |
------------------------------------------------------------------------------
Launch Browser Tab                                                    | FAIL |
Element with locator 'xpath://div[contains(@class, "jp-FileBrowser-toolbar")]//*[contains(text(), "New")]' not found.
------------------------------------------------------------------------------
Launch Another Browser Tab                                            | FAIL |
Element with locator 'xpath://div[contains(@class, "jp-FileBrowser-toolbar")]//*[contains(text(), "New")]' not found.
------------------------------------------------------------------------------
Acceptance.Notebook :: Server Proxies in Notebook                     | FAIL |
3 tests, 1 passed, 2 failed
==============================================================================
Acceptance :: Acceptance tests for jupyter-server-proxy               | FAIL |
6 tests, 4 passed, 2 failed
==============================================================================
Output:  /home/erik/dev/jupyterhub/jupyter-server-proxy/build/robot/output.xml
Log:     /home/erik/dev/jupyterhub/jupyter-server-proxy/build/robot/log.html
Report:  /home/erik/dev/jupyterhub/jupyter-server-proxy/build/robot/report.html
---------------------------------------------------------------------------------------------------------- Captured stderr call ----------------------------------------------------------------------------------------------------------
[ WARN ] Error in file '/home/erik/miniforge3/lib/python3.10/site-packages/JupyterLibrary/common/CodeMirror.resource' on line 24: The '[Return]' setting is deprecated. Use the 'RETURN' statement instead.
[ WARN ] Error in file '/home/erik/miniforge3/lib/python3.10/site-packages/JupyterLibrary/common/CodeMirror.resource' on line 39: The '[Return]' setting is deprecated. Use the 'RETURN' statement instead.
[ WARN ] Error in file '/home/erik/miniforge3/lib/python3.10/site-packages/JupyterLibrary/common/CodeMirror.resource' on line 46: The '[Return]' setting is deprecated. Use the 'RETURN' statement instead.
[ WARN ] Error in file '/home/erik/miniforge3/lib/python3.10/site-packages/JupyterLibrary/common/CodeMirror.resource' on line 53: The '[Return]' setting is deprecated. Use the 'RETURN' statement instead.
[ WARN ] Error in file '/home/erik/miniforge3/lib/python3.10/site-packages/JupyterLibrary/common/CodeMirror.resource' on line 61: The '[Return]' setting is deprecated. Use the 'RETURN' statement instead.
[ WARN ] Error in file '/home/erik/miniforge3/lib/python3.10/site-packages/JupyterLibrary/common/CodeMirror.resource' on line 68: The '[Return]' setting is deprecated. Use the 'RETURN' statement instead.
[ WARN ] Error in file '/home/erik/miniforge3/lib/python3.10/site-packages/JupyterLibrary/common/CodeMirror.resource' on line 76: The '[Return]' setting is deprecated. Use the 'RETURN' statement instead.
[ WARN ] Error in file '/home/erik/miniforge3/lib/python3.10/site-packages/JupyterLibrary/common/CodeMirror.resource' on line 85: The '[Return]' setting is deprecated. Use the 'RETURN' statement instead.
[ WARN ] Error in file '/home/erik/miniforge3/lib/python3.10/site-packages/JupyterLibrary/clients/jupyterlab/Shortcuts.resource' on line 14: The '[Return]' setting is deprecated. Use the 'RETURN' statement instead.
[ WARN ] Error in file '/home/erik/miniforge3/lib/python3.10/site-packages/JupyterLibrary/clients/jupyterlab/Icons.resource' on line 38: The '[Return]' setting is deprecated. Use the 'RETURN' statement instead.
[ WARN ] Error in file '/home/erik/miniforge3/lib/python3.10/site-packages/JupyterLibrary/clients/jupyterlab/Icons.resource' on line 47: The '[Return]' setting is deprecated. Use the 'RETURN' statement instead.
[ WARN ] Error in file '/home/erik/miniforge3/lib/python3.10/site-packages/JupyterLibrary/clients/jupyterlab/PageInfo.resource' on line 28: The '[Return]' setting is deprecated. Use the 'RETURN' statement instead.
[ WARN ] Error in file '/home/erik/miniforge3/lib/python3.10/site-packages/JupyterLibrary/clients/jupyterlab/PageInfo.resource' on line 57: The '[Return]' setting is deprecated. Use the 'RETURN' statement instead.
[ WARN ] Error in file '/home/erik/miniforge3/lib/python3.10/site-packages/JupyterLibrary/clients/jupyterlab/Settings.resource' on line 19: The '[Return]' setting is deprecated. Use the 'RETURN' statement instead.
[ WARN ] Error in file '/home/erik/miniforge3/lib/python3.10/site-packages/JupyterLibrary/clients/jupyterlab/Settings.resource' on line 30: The '[Return]' setting is deprecated. Use the 'RETURN' statement instead.
[ WARN ] Error in file '/home/erik/miniforge3/lib/python3.10/site-packages/JupyterLibrary/clients/jupyterlab/Settings.resource' on line 62: The '[Return]' setting is deprecated. Use the 'RETURN' statement instead.
[ WARN ] Error in file '/home/erik/miniforge3/lib/python3.10/site-packages/JupyterLibrary/clients/jupyterlab/Shell.resource' on line 84: The '[Return]' setting is deprecated. Use the 'RETURN' statement instead.

Release 0.5.0a0

  • merge all outstanding PRs
  • ensure the versions have been bumped (check with doit)
  • ensure HISTORY.ipynb is up-to-date
    • move the new release to the top of the stack
  • validate on binder
  • validate on ReadTheDocs
  • wait for a successful build of main
  • download the dist archive and unpack somewhere (maybe a fresh dist)
  • create a new release through the GitHub UI
    • paste in the relevant HISTORY.ipynb entries
    • upload the artifacts
  • actually upload to pypi.org
    cd dist
    twine upload *.tar.gz *.whl
  • postmortem

Verify no keywords collide with Robot Framework standard library

Description

Some custom keywords presently have the same names as keywords from Robot Framework standard library keywords (specifically XML).

Reproduce

  • Use all the keywords
  • see some warnings

Expected behavior

  • no warnings about shadowed keywords

Context

  • add a lint step that checks libdoc output against all standard library

Add GitHub Actions

The azure stuff is pretty broken at this point. Let's move over to GitHub Actions. I'm thinking of adding doit and conda-lock to the setup, which plays pretty nice with setup-miniconda.

Add/automate AUTHORS

Some nice folks have helped out on this repo. Should figure out a way to automate recognizing them in the docs.

Add and Run Jupyterlab code cell error

Hello,

I'm trying to execute the following from robotframework-jupyterlibrary:

Check Something
[Documentation] Check Something
Add And Run JupyterLab Code Cell print("something")

If fails with the following error:

image

I have the following libraries:
robotframework 4.1.2
robotframework-jupyterlibrary 0.4.0
robotframework-otp 1.1.0
robotframework-pythonlibcore 3.0.0
robotframework-seleniumlibrary 6.0.0

Could you please advise?

Add more features from copy-paste downstreams

In jupyterlab-lsp and probably scattered a few other places, there are a trove of lab- (but not extension-) specific options, which would be nice to polish and document, and include in this repo:

  • more advanced codemirror behaviors
  • working with advanced settings
  • working with jupyter_notebook_config.json
  • better support of starting servers without shell and peril-fraught escaping
  • more encapsulated notebook/lab config and runtime files (e.g. workspaces)
  • "nasty" paths with non-ASCII and spaces to catch more encoding/escaping errors

Release 0.4.2

  • merge all outstanding PRs
  • ensure the versions have been bumped (check with doit)
  • ensure HISTORY.ipynb is up-to-date
    • move the new release to the top of the stack
  • validate on binder
    • seems to be having some NYE hangover, returning Unaccessible... skipping for today

  • validate on ReadTheDocs
  • wait for a successful build of master
  • download the dist archive and unpack somewhere (maybe a fresh dist)
  • create a new release through the GitHub UI
    • paste in the relevant HISTORY.ipynb entries
    • upload the artifacts
  • actually upload to pypi.org
    cd dist
    twine upload *.tar.gz *.whl
  • postmortem

Support JupyterLab 2.0

A fair number of selectors have changed, and some potentially useful things (like disabling build notification) are now available.

"Add and Run JupyterLab Code Cell" keyword throws error intermittently

robotframework==4.0.3
robotframework-jupyterlibrary==0.3.1
Jupyterlab version: 3.2.8

Observation:

While we try to use the keyword "Add and Run JupyterLab Code Cell", sometimes it works and sometimes it throws the below error:

ElementNotInteractableException: Message: Element <svg> could not be scrolled into view
Stacktrace:
WebDriverError@chrome://remote/content/shared/webdriver/Errors.jsm:183:5
ElementNotInteractableError@chrome://remote/content/shared/webdriver/Errors.jsm:293:5
webdriverClickElement@chrome://remote/content/marionette/interaction.js:156:11
interaction.clickElement@chrome://remote/content/marionette/interaction.js:125:11
clickElement@chrome://remote/content/marionette/actors/MarionetteCommandsChild.jsm:203:24
receiveMessage@chrome://remote/content/marionette/actors/MarionetteCommandsChild.jsm:91:31

Attached the screenshots for reference.
Screenshot_1
Screenshot_2

Add coverage keywords

Elevator Pitch

Capture client, server, and kernel coverage with robot tests.

Motivation

Being able to gather fine-grained coverage of JS, python, and maybe other languages from a single test increases the value of executed robot tests.

Design Ideas

Client coverage:

Server coverage:

Kernel coverage could be gathered by copyinh a custom kernel.json, and adding some more data:

{
 "argv": [
  "$PREFIX/bin/python",
  "-m",
  "coverage", "run", "-m", // ... add this line, with more args
  "ipykernel_launcher",
  "-f",
  "{connection_file}"
 ],
 "display_name": "Python 3 (ipykernel)", // add the context name?
 "language": "python",
 "metadata": {
  "debugger": true
 }
}

Finally, some reporting keywords could be added, optionally embedding the reports inside the robot HTML if they can be generated as a single file.

Schedule weekly test runs against unfrozen dependencies

With tests scheduled to run regularly against dependencies that aren't frozen, we could detect issues arising over time due to changes in dependencies or the various execution environments, such as the GitHub actions runners having certain python unrelated dependencies such as versions of geckodriver etc.

Below is an example of triggers for a github actions test workflow that could be used.

on:
  pull_request:
    paths-ignore:
      - "docs/**"
      - "contrib/**"
      - "**.md"
      - ".github/workflows/*"
      - "!.github/workflows/test.yaml"
  push:
    paths-ignore:
      - "docs/**"
      - "contrib/**"
      - "**.md"
      - ".github/workflows/*"
      - "!.github/workflows/test.yaml"
    branches-ignore:
      - "dependabot/**"
      - "pre-commit-ci-update-config"
  schedule:
    # At 05:00 on Monday - https://crontab.guru
    - cron: "0 5 * * 1"
  workflow_dispatch:

Re-run tests to verify function with jupyter_server 2 now released

I'm not grasping how everything works, but I think there is a test suite setup in this repo to run a test involving jupyter_server. Can we run that test suite now that jupyter_server 2 is out and see if it succeeds, or if there is something to fix in this repo?

I was led to think about this as I observed the failure noticed "parent suite setup failed" and was thinking perhaps that was related to this repo rather than the failing test in the jupyterhub/jupyter-server-proxy relying on this project.

image

Related source code in this repo

dependencies:
- jupyter_server >=1.2

matrix:
# The Matrix
#
# This is the single source-of-truth of what we currently support
# - the matrix (with excludes) is parsed in project.py
# - the excursions are in env_specs, e.g. lab1.yml
# - to regenerate the specs, requires `conda-lock`:
#
# rm -rf .github/locks
# doit -n4 lock
#
conda-subdir: [win-64, osx-64, linux-64]
python-version: [py3.7, py3.10]
lab-version: [lab1, lab2, lab3]

Binder fails to build

Looks like the Binder is currently failing to build with the following:

image

Maybe it's a matter of regenerating the conda.lock file.

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.