GithubHelp home page GithubHelp logo

photo / openphoto-python Goto Github PK

View Code? Open in Web Editor NEW
42.0 42.0 16.0 934 KB

A Python OAuth client for OpenPhoto

Home Page: http://theopenphotoproject.org

License: Apache License 2.0

Python 99.39% Shell 0.61%

openphoto-python's People

Contributors

jmathai avatar jmcfarlane avatar oscherler avatar sneakypete81 avatar walkah 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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

openphoto-python's Issues

Delete methods should return boolean

The tag/album/photo delete methods really ought to return the "True/False" return value from the API call.

I'm not sure if the API would ever return False without also raising an error code (which would raise an exception), but it's worth making this consistent anyway.

use setuptools/distribute for setup.py (optionally?) vs distutils

setuptools and distribute allow you to python setup.py develop as well as to install dependencies; i.e. we require oauth2 (https://github.com/openphoto/openphoto-python/blob/master/setup.py#L9) but it is not actually installed with python setup.py install. I would recommend just requiring setuptools or distribute, but you can also optionally support them in the same setup.py; see https://github.com/pypa/virtualenv/blob/develop/setup.py for example.

I'm happy to do this if there is interest (or at least no dissent)

doesn't work with https

in my config file I wrote the host without "https" and I get this error :

Traceback (most recent call last):
  File "./import.py", line 23, in <module>
    resp = client.get("/photos/list.json")
  File "/usr/local/lib/python2.6/dist-packages/trovebox/http.py", line 92, in get
    return self._process_response(response)
  File "/usr/local/lib/python2.6/dist-packages/trovebox/http.py", line 188, in _process_response
    (response.status_code, response.reason))
trovebox.errors.Trovebox404Error: HTTP Error 404: Not Found

I can correct this by replacing "http" with "https" at line 69 in http.py : url = urlunparse(('http', self.host, endpoint, '', '', ''))
The problem is this urlunparse.

We probably need a scheme parsing in http.init to avoid the use of "http" hard written in http.py.

so we perhaps need a

error with StringIO cannot read config_file

Hi,
I'm not sure it's an issue due to trovebox or to my OS, but in auth.py it can't read my config file anymore.

I changed it to :

    with open(config_path, "r") as conf:
        res = conf.read()
        buf.write(unicode(res))

and it works now.

It seems to me it is some kind of issue with having python3 and python2.7 on the same system. With StringIO I can't use the "open" method.

More unit tests

As mentioned in #37, the majority of the current tests are integration tests, communicating via the API with a photo/frontend server. Multiple test servers can be used to validate against different server/API versions.

While this is a useful strategy for flushing out issues and generally making sure the API doesn't accidentally get broken, it would also be useful to have a wider variety of unit tests that don't require a server connection.

In particular:

  • Get/Post testing of openphoto_http.py
  • Each API endpoint
  • The commandline interface

The goal should be to mock at the socket level (eg. Httpretty).

Once we have good unit test coverage, we can use Travis to run unit tests, and run the integration tests manually as required.

CLI does not perform tilde expansion

trovebox -c test -X POST -e /photo/upload.json -F photo=@~/Pictures/opmeqnqXBX-28cbef_960x180.jpg
Traceback (most recent call last):
  File "/usr/local/bin/trovebox", line 9, in <module>
    load_entry_point('trovebox==0.5', 'console_scripts', 'trovebox')()
  File "/usr/local/lib/python2.7/dist-packages/trovebox/main.py", line 90, in main
    params, files = extract_files(params)
  File "/usr/local/lib/python2.7/dist-packages/trovebox/main.py", line 126, in extract_files
    files[name] = open(params[name][1:], 'rb')
IOError: [Errno 2] No such file or directory: '~/Pictures/opmeqnqXBX-28cbef_960x180.jpg'

But it works if /home/pete/file.jpg is used instead of ~/file.jpg

accept use of self-signed ssl certificate

Hi,
When I try to connect to my self hosted trovebox site, I get this error.

Traceback (most recent call last): File "./import.py", line 24, in <module> resp = client.get("/photos/list.json") File "/usr/local/lib/python2.6/dist-packages/trovebox/http.py", line 81, in get response = session.get(url, params=params, auth=auth, verify=False) File "/usr/local/lib/python2.6/dist-packages/requests/sessions.py", line 254, in get return self.request('get', url, **kwargs) File "/usr/local/lib/python2.6/dist-packages/requests/sessions.py", line 241, in request r.send(prefetch=prefetch) File "/usr/local/lib/python2.6/dist-packages/requests/models.py", line 518, in send r = self.auth(self) File "/usr/local/lib/python2.6/dist-packages/requests_oauthlib/core.py", line 58, in __call__ if is_form_encoded or extract_params(r.body): AttributeError: 'Request' object has no attribute 'body'

I can't understand why but I suspect that it is because of https + self signed certificate, and I can't find a way to disable certificate checking.

Make the library more Pythonic

I started using this (excellent!) Python library, and pretty quickly found myself wrapping the raw GET/POST calls up into Python objects.

So instead of:

resp = client.get("/photos/list.json")
resp = client.post("/photo/62/update.json", {'tags': 'tag1,tag2'})

I can:

photos = client.photos_list()
photos[0].update(tags=["tag1", "tag2"])

This would hopefully make the library easier to use, since it abstracts the API URL constructions and responses.

@jmathai, @walkah: what do you think? Is this useful, or beyond what we should be doing here?

Error 500 "Invalid mime type"

Hello,

when I try to upload a photo by command line I have this error from the server:

{
"code":500,
"message":"Invalid mime type",
"result":false
}

I exec this command line:

openphoto -p -X POST -H akirasan.trovebox.com -e /photo/upload.json -F 'photo=/mnt/ksi2t/scripts/DSC_8422.JPG' -F 'tags=test'

The file is a jpeg image. Others request to the server trovebox.com are succesfull.

Regards,
akirasan

Properly handle Unicode characters in post params

From: https://gist.github.com/1732810

Processing 906 of 3975 2768847584.json ...
Traceback (most recent call last):
  File "import.py", line 66, in <module>
    main()
  File "import.py", line 44, in main
    resp = client.post('/photo/upload.json', params)
  File "/Library/Python/2.7/site-packages/openphoto-0.1-py2.7.egg/openphoto/__init__.py", line 41, in post
    body = urllib.urlencode(params)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/urllib.py", line 1294, in urlencode
    v = quote_plus(str(v))
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe7' in position 77: ordinal not in range(128)

use a dotfile (optionally) vs forcing secrets to live in the environment

Currently the openphoto script gets the credentials from rather generically-named environment variables:

https://github.com/openphoto/openphoto-python/blob/master/README.markdown

If you're very often running this program, it is probably better to have these secrets live in a dotfile instead of having a two-step process to run the script (source a script then run openphoto). I would advise looking for a dotfile, e.g. $HOME/.openphoto, which contains the credentials and overriding from the environment if the environment variables exist

Python 3 Support

I've had a crack at this, with the aim of supporting both Python 2 and 3 from the same codebase. This involved:

  • Running 2to3.py
  • oauth2 doesn't support Python 3, so replaced with requets-oauthlib (this also cleans up a bit of multipart post magic)
  • Cleaning up (using PyLint)

As a result of the above, we can support Python 2.6, 2.7 and 3.3. Maintaining support for 2.5 at the same time as 3.x is difficult (and messy).

To avoid conflicts, I'd like to wait until the last few PRs are merged before doing any more.

Any comments?

Uploading files with non ASCII characters fails

I tried to upload a file using the command line tool:

  trovebox  -p -X POST -e /photo/upload.json -F 'photo=@/Users/me/Dropbox/Images/Älgkalv/ÄLGKALV.jpg'

But if the filename contains non ascii characters it fails with the following error message:

UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 10: ordinal not in range(128)

Non ascii characters in the path is no problem:

trovebox  -p -X POST -e /photo/upload.json -F 'photo=@/Users/me/Dropbox/Images/Älgkalv/ALGKALV.jpg'

I edited main.py line 126:

        files[name] = open(os.path.expanduser(params[name][1:].decode('utf-8')), 'rb')

This solves the UnicodeDecodeError, but then the authention fails:

Traceback (most recent call last):
File "/usr/local/bin/trovebox", line 9, in
load_entry_point('trovebox==0.6.1', 'console_scripts', 'trovebox')()
File "/Library/Python/2.7/site-packages/trovebox/main.py", line 93, in main
files=files, **params)
File "/Library/Python/2.7/site-packages/trovebox/http.py", line 180, in post
(response.status_code, response.reason))
trovebox.errors.TroveboxError: HTTP Error 401: Unauthorized

if i check the apache error log, this is what i see when the authentication fails:

[Mon Dec 09 19:30:40.403222 2013] [:error] [pid 4995] [client 192.168.5.106:57220] {severity:warn, description:"oauth_problem=signature_invalid&debug_sbs=POST&http%3A%2F%2Fphoto.majsajt.com%2Fphoto%2Fupload.json&oauth_consumer_key%3F73048bee39a55e5db2a4a97fe421f5%26oauth_nonce%3D2567566948544325531386613844%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1386613844%26oauth_token%3D4f8f8d5a960af9e64fc2529075ffac%26oauth_version%3D1.0%26photo%3D%25FF%25D8%25FF%25E1Q%25CCExif%2500%2500II%252A%2500%2508%2500%2500%2500%250A%2500%250F%2501%2502%2500%2506%2500%2500%2500%2586%2500%2500%2500%2510%2501%2502%2500%250E%2500%2500%2500%258C%2500%2500%2500%2512%2501%2503%2500%2501%2500%2500%2500%2501%2500%2500%2500%251A%2501%2505%2500%2501%2500%2500%2500%25AC%2500%2500%2500%251B%2501%2505%2500%2501%2500%2500%2500%25B4%2500%2500%2500%2528%2501%2503%2500%2501%2500%2500%2500%2502%2500%2500%25002%2501%2502%2500%2514%2500%2500%2500%25BC%2500%2500%2500%2513%2502%2503%2500%2501%2500%2500%2500%2502%2500%2500%2500i%2587%2504%2500%2501%2500%2500%2500%25D0%2500%2500%2500%2525%2588%2504%2500%2501%2500%2500%2500%25DE%2521%2500%2500%25E0%2528%2500%2500Canon%2500Canon%2520EOS%252040D%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500H%2500%2500%2500%2501%2500%2500%2500H%2500%2500%2500%2501%2500%2500%25002008%253A05%253A24%252014%253A15%253A01%2500%251F%2500%259A%2582%2505%2500%2501%2500%2500%2500J%2502%2500%2500%259D%2582%2505%2500%2501%2500%2500%2500R%2502%2500%2500%2522%2588%2503%2500%2501%2500%2500%2500%2502%2500%2500%2500%2527%2588%2503%2500%2501%2500%2500%2500%2590%2501%2500%2500%2500%2590%2507%2500%2504%2500%2500%25000221%2503%2590%2502%2500%2514%2500%2500%2500Z%2502%2500%2500%2504%2590%2502%2500%2514%2500%2500%2500n%2502%2500%2500%2501%2591%2507%2500%2504%2500%2500%2500%2501%2502%2503%2500%2501%2592%250A%2500%2501%2500%2500%2500%2582%2502%2500%2500%2502%2592%2505%2500%2501%2500%2500%2500%258A%2502%2500%2500%2504%2592%250A%2500%2501%2500%2500%2500%2592%2502%2500%2500%2507%2592%2503%2500%2501%2500%2500%2500%2505%2500%2500%2500%2509%2592%2503%2500%2501%2500%2500%2500%2510%2500%2500%2500%250A%2592%2505%2500%2501%2500%2500%2500%259A%2502%2500%2500%257C%2592%2507%2500%2506%251E%2500%2500%25A2%2502%2500%2500%2586%2592%2507%2500%2508%2501%2500%2500%25A8%2520%2500%2500%2590%2592%2502%2500%2503%2500%2500%250000%2500%2500%2591%2592%2502%2500%2503%2500%2500%250000%2500%2500%2592%2592%2502%2500%2503%2500%2500%250000%2500%2500%2500%25A0%2507%2500%2504%2500%2500%25000100%2501%25A0%2503%2500%2501%2500%2500%2500%2501%2500%2500%2500%2502%25A0%2503%2500%2501%2500%2500%25000%250F%2500%2500%2503%25A0%2503%2500%2501%2500%2500%2500%2520%250A%2500%2500%2505%25A0%2504%2500%2501%2500%2500%2500%25B0%2521%2500%2500%250E%25A2%2505%2500%2501%2500%2500%2500%25CE%2521%2500%2500%250F%25A2%2505%2500%2501%2500%2500%2500%25D6%2521%2500%2500%2510%25A2%2503%2500%2501%2500%2500%2500%2502%2500%2500%2500%2501%25A4%2503%2500%2501%2500%2500%2500%2500%2500%2500%2500%2502%25A4%2503%2500%2501%2500%2500%2500%2500%2500%2500%2500%2503%25A4%2503%2500%2501%2500%2500%2500%2500%2500%2500%2500%2506%25A4%2503%2500%2501%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2501%2500%2500%2500%25FA%2500%2500%2500G%2500%2500%2500%250A%2500%2500%25002008%253A05%253A24%252014%253A15%253A01%25002008%253A05%253A24%252014%253A15%253A01%2500%2500%2500%2508%2500%2500%2500%2501%2500%2500%25A0%2505%2500%2500%2500%2501%2500%2500%2500%2500%2500%2501%2500%2500%2500%25C2%2500%2500%2500%2501%2500%2500%2500%2521%2500%2501%2500%2503%2500%252F%2500%2500%25004%2504%2500%2500%2502%2500%2503%2500%2504%2500%2500%2500%2592%2504%2500%2500%2503%2500%2503%2500%2504%2500%2500%2500%259A%2504%2500%2500%2504%2500%2503%2500%2522%2500%2500%2500%25A2%2504%2500%2500%2506%2500%2502%2500%250E%2500%2500%2500%25E6%2504%2500%2500%2507%2500%2502%2500%2518%2500%2500%2500%2506%2505%2500%2500%2509%2500%2502%2500%2520%2500%2500%2500%251E%2505%2500%2500%250C%2500%2504%2500%2501%2500%2500%2500O%2588%25FD%2584%250D%2500%2507%2500%2500%250C%2500%2500%253E%2505%2500%2500%2510%2500%2504%2500%2501%2500%2500%2500%2590%2501%2500%2580%2513%2500%2503%2500%2504%2500%2500%2500%253E%2511%2500%2500%2515%2500%2504%2500%2501%2500%2500%2500%2500%2500%2500%25A0%2519%2500%2503%2500%2501%2500%2500%2500%2501%2500%2500%2500%2526%2500%2503%25000%2500%2500%2500F%2511%2500%2500%2583%2500%2504%2500%2501%2500%2500%2500%2500%2500%2500%2500%2593%2500%2503%2500%2518%2500%2500%2500%25A6%2513%2500%2500%2595%2500%2502%2500%2540%2500%2500%2500%25D6%2513%2500%2500%2596%2500%2502%2500%2510%2500%2500%2500%2516%2514%2500%2500%2597%2500%2507%2500%2500%2504%2500%2500%2526%2514%2500%2500%2598%2500%2503%2500%2504%2500%2500%2500%2526%2518%2500%2500%2599%2500%2504%2500Y%2500%2500%2500.%2518%2500%2500%259A%2500%2504%2500%2505%2500%2500%2500%2592%2519%2500%2500%25A0%2500%2503%2500%250E%2500%2500%2500%25A6%2519%2500%2500%25AA%2500%2503%2500%2506%2500%2500%2500%25C2%2519%2500%2500%25B4%2500%2503%2500%2501%2500%2500%2500%2501%2500%2500%2500%25D0%2500%2504%2500%2501%2500%2500%2500%2500%2500%2500%2500%25E0%2500%2503%2500%2511%2500%2500%2500%25CE%2519%2500%2500%2501%2540%2503%2500%25B4%2502%2500%2500%25F0%2519%2500%2500%2508%2540%2503%2500%2503%2500%2500%2500X%251F%2500%2500%2509%2540%2503%2500%2503%2500%2500%2500%255E%251F%2500%2500%2510%2540%2502%2500%2520%2500%2500%2500d%251F%2500%2500%2511%2540%2507%2500%25FC%2500%2500%2500%2584%251F%2500%2500%2512%2540%2502%2500%2520%2500%2500%2500%2580%2520%2500%2500%2500%2500%2500%2500%255E%2500%2502%2500%2500%2500%2503%2500%2500%2500%2500%2500%2500%2500%2502%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%25FF%257F%25FF%257F%2503%2500%2502%2500%2500%2500%2500%2500%25FF%25FF1%2500%25FA%25007%2500%2501%2500%25A0%2500%2540%2501%2500%2500%2500%2500%2500%2500%2500%2500%25FF%25FF%25FF%25FF%25FF%25FF%2500%2500%2500%2500%2500%2500%2500%2500%25FF%25FF%25FF%25FF%2500%2500%2500%2500%25FF%257F%25FF%25FF%25FF%25FF%25FF%25FF%2500%2500%25C2%2500a%25B0%2510%251C%2500%2500%2500%2500%2500%2500%2500%2500D%2500%2500%2500%25E0%2500%25D8%2500%25B4%2500%2500%2501%2500%2500%2500%2500%2503%2500%2500%2500%2508%2500%2508%2500%2599%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2501%2500%2500%2500%2500%2500%25B4%2500%2504%2501%258E%2500%2500%2500%2500%2500%25F8%2500%25FF%25FF%25FF%25FF%25FF%25FF%25FF%25FF%2500%2500%2500%2500%2500%2500Canon%2520EOS%252040D%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500Firmware%2520Version%25201.1.1%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%25AA%25AAy5x5X%2500%258E%258E%2500%2503%2500%2500%2500%2500%2500%2500%2501%2500%2500%2506%2500%2500%2599%25BA%255Bc%2500%2500%25C2%2500%2502%2500%2500%2500%2500%2501%25BB%25BB%2501%2501%2500%2500%2500%2500%2502%2501%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2504%25E3%2504-%250C%250C%2509%25F9%2511j%2515%2588%251C%2595%2523F%2527%2524%25CC%25CC%2509%2500%2500%2500%2500%2500%2500%2500%2503%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%25FF%2501%2500%2500%2500%2500%2500%2500P%2514%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2501%2500%2500%2500%2501%2500%2500%2500%2501%2500%2500%2500%2503%2500%2500%2500%2503%2500%2500%2500%2503%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2581%2500%2500%2500%2501%2500%2500%2500%2507%2500%2500%2500%2500%2500%2500%2500%2503%2500%2500%2500%2507%2500%2500%2500%2500%2500%2500%2500%2508%2500%2500%2500%2500%2500%2500%2500%2500%25FF%25FB%25FF%2500%2500%2500%2500%2500%2500%2500%2500%25010X%25001%25007%2500%25FA%2591u%25BA%251F%2500%2500%2500%2500%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%25001.1.1%25006C%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500d%2500%2500%2500d%2500%2500%2500d%2500%2500%2500%2524%2509%2500%2500%2500%2500%2500%2500%2500%2500%250

Full error message:

$ trovebox -p -X POST -e /photo/upload.json -F 'photo=@/Users/me/Dropbox/Images/Älgkalv/ÄLGKALV.jpg'
Traceback (most recent call last):
File "/usr/local/bin/trovebox", line 9, in
load_entry_point('trovebox==0.6.1', 'console_scripts', 'trovebox')()
File "/Library/Python/2.7/site-packages/trovebox/main.py", line 93, in main
files=files, *_params)
File "/Library/Python/2.7/site-packages/trovebox/http.py", line 152, in post
files=files, auth=auth)
File "/Library/Python/2.7/site-packages/requests/sessions.py", line 403, in post
return self.request('POST', url, data=data, *_kwargs)
File "/Library/Python/2.7/site-packages/requests/sessions.py", line 324, in request
prep = self.prepare_request(req)
File "/Library/Python/2.7/site-packages/requests/sessions.py", line 265, in prepare_request
hooks=merge_setting(request.hooks, self.hooks),
File "/Library/Python/2.7/site-packages/requests/models.py", line 286, in prepare
self.prepare_body(data, files)
File "/Library/Python/2.7/site-packages/requests/models.py", line 416, in prepare_body
(body, content_type) = self._encode_files(files, data)
File "/Library/Python/2.7/site-packages/requests/models.py", line 141, in _encode_files
rf.make_multipart(content_type=ft)
File "/Library/Python/2.7/site-packages/requests/packages/urllib3/fields.py", line 175, in make_multipart
self.headers['Content-Disposition'] += '; '.join(['', self._render_parts((('name', self._name), ('filename', self._filename)))])
File "/Library/Python/2.7/site-packages/requests/packages/urllib3/fields.py", line 138, in _render_parts
parts.append(self._render_part(name, value))
File "/Library/Python/2.7/site-packages/requests/packages/urllib3/fields.py", line 118, in _render_part
return format_header_param(name, value)
File "/Library/Python/2.7/site-packages/requests/packages/urllib3/fields.py", line 43, in format_header_param
result.encode('ascii')
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 10: ordinal not in range(128)

Retry if a request fails

Very occasionally API requests to the hosted site fail:

Perhaps I should raise a photo/frontend issue? I never see this when running on a self-hosted server, so I can't look at the logs.

It would be nice if the Python library automatically retried these failed requests a couple of times. I'm not sure if we'd want to support this for all failures, or just a subset (eg. 500 errors).

If we do this, the number of retries to be configurable (or set to 0 to disable).

Submit to PyPI

Uploading to PyPI will make this more straightforward to install, and means it can be specified as an auto-installed dependency.

Depends on #22 (rename to Trovebox).

Provide the necessary command-line steps to create OAuth tokens

At the moment, it is very hard to find the details on how to connect via oauth IMHO.

The script openphoto should IMHO be able to prompt the user and direct him/her in what needs to be done to connect with OAuth.

It should probably do things like print "go to your browser at http:///... and click ok" in the flow of validating token requests and such.

Hope this helps.

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.