Giter Site home page Giter Site logo

babashka.curl's Introduction

babashka.curl

Clojars Project

A tiny curl wrapper via idiomatic Clojure, inspired by clj-http, Ring and friends.

NOTE: This library is mostly replaced by babashka.http-client which is mostly API-compatible with babashka.curl. The babashka.http-client library is built-in as of babashka version 1.1.171.

Status

This library is part of babashka but can also be used with JVM Clojure. Check CHANGES.md before upgrading as the API may still undergo some changes. Contributions welcome.

Usage

(require '[babashka.curl :as curl])
(require '[clojure.java.io :as io]) ;; optional
(require '[cheshire.core :as json]) ;; optional

GET

Simple GET request:

(curl/get "https://httpstat.us/200")
;;=> {:status 200, :body "200 OK", :headers { ... }}

Headers

Passing headers:

(def resp (curl/get "https://httpstat.us/200" {:headers {"Accept" "application/json"}}))
(json/parse-string (:body resp)) ;;=> {"code" 200, "description" "OK"}

Query parameters

Query parameters:

(->
  (curl/get "https://postman-echo.com/get" {:query-params {"q" "clojure"}})
  :body
  (json/parse-string true)
  :args)
;;=> {:q "clojure"}

To send multiple params to the same key:

;; https://postman-echo.com/get?q=clojure&q=curl
(curl/get "https://postman-echo.com/get" {:query-params [[:q "clojure"] [:q "curl"]]})

POST

A POST request with a :body:

(def resp (curl/post "https://postman-echo.com/post" {:body "From Clojure"}))
(json/parse-string (:body resp)) ;;=> {"args" {}, "data" "", ...}

Posting a file as a POST body:

(:status (curl/post "https://postman-echo.com/post" {:body (io/file "README.md")}))
;; => 200

Posting a stream as a POST body:

(:status (curl/post "https://postman-echo.com/post" {:body (io/input-stream "README.md")}))
;; => 200

Posting form params:

(:status (curl/post "https://postman-echo.com/post" {:form-params {"name" "Michiel"}}))
;; => 200

Basic auth

Basic auth:

(:body (curl/get "https://postman-echo.com/basic-auth" {:basic-auth ["postman" "password"]}))
;; => "{\"authenticated\":true}"

Download binary

Download a binary file:

(io/copy
  (:body (curl/get "https://github.com/babashka/babashka/raw/master/logo/icon.png"
    {:as :bytes}))
  (io/file "icon.png"))
(.length (io/file "icon.png"))
;;=> 7748

Streaming

With :as :stream

(:body (curl/get "https://github.com/babashka/babashka/raw/master/logo/icon.png"
    {:as :stream}))

will return the raw input stream.

Passing through arguments

Passing raw arguments to curl can be done with :raw-args:

(:status (curl/get "http://www.clojure.org" {:raw-args ["--max-redirs" "0"] :throw false}))
;;=> 301

Unix sockets

Talking to a UNIX socket:

(-> (curl/get "http://localhost/images/json"
              {:raw-args ["--unix-socket"
                          "/var/run/docker.sock"]})
    :body
    (json/parse-string true)
    first
    :RepoTags)
;;=> ["babashka/babashka:0.0.79-SNAPSHOT"]

URL construction

Using the low-level API for fine grained(and safer) URL construction:

(-> (curl/request {:url {:scheme "https"
                         :host   "httpbin.org"
                         :port   443
                         :path   "/get"
                         :query  "q=test"}})
    :body
    (json/parse-string true))
;;=>
{:args {:q "test"},
 :headers
 {:Accept "*/*",
  :Host "httpbin.org",
  :User-Agent "curl/7.64.1",
  :X-Amzn-Trace-Id
  "Root=1-5e63989e-7bd5b1dba75e951a84d61b6a"},
 :origin "46.114.35.45",
 :url "https://httpbin.org/get?q=test"}

Redirects

Redirects are automatically followed. To opt out of this behaviour, set :follow-redirects to false.

(curl/get "https://httpstat.us/302" {:follow-redirects false})

Exceptions

An ExceptionInfo will be thrown for all HTTP response status codes other than #{200 201 202 203 204 205 206 207 300 301 302 303 304 307} or if curl exited with a non-zero exit code. The response map is the exception data.

(curl/get "https://httpstat.us/404")
;;=> Execution error (ExceptionInfo) at babashka.curl/request (curl.clj:228).
     status 404

(:status (ex-data *e))
;;=> 404

To opt out of an exception being thrown, set :throw to false.

(:status (curl/get "https://httpstat.us/404" {:throw false}))
;;=> 404

If the body is being returned as a stream then exceptions are never thrown and the :exit value is wrapped in a Delay.

(:exit (curl/get "https://httpstat.us/404" {:as :stream}))
;;=> #object[clojure.lang.Delay 0x75769ab0 {:status :pending, :val nil}]
(force *1)
;;=> 0

Error output

Error output can be found under the :err key:

(:err (curl/get "httpx://postman-echo.com/get" {:throw false}))
;;=> "curl: (1) Protocol \"httpx\" not supported or disabled in libcurl\n"

Compression

From babashka 0.2.4 onwards, this library will call curl with --compressed by default. To opt out, pass :compressed false in the options. On Windows 10 the default installed curl does not support this option. You can either upgrade curl or perform all requests using :compressed false.

Debugging requests

Set :debug to true to get debugging information along with the response. The :command value contains the command that was executed to obtain the response. The :options value contains options that were used to construct the command. Note that all of these values are for debugging only and contain implementation details that may change in the future.

(def resp (curl/head "https://postman-echo.com/head" {:debug true}))
(:command resp)
;;=> ["curl" "--silent" "--show-error" "--location" "--dump-header" "/var/folders/2m/h3cvrr1x4296p315vbk7m32c0000gp/T/babashka.curl16567082489957878064.headers" "--head" "https://postman-echo.com/head"]
(:options resp)
;;=> {:debug true, :url "https://postman-echo.com/head", :method :head, :header-file #object[java.io.File 0x61d34b4 "/var/folders/2m/h3cvrr1x4296p315vbk7m32c0000gp/T/babashka.curl16567082489957878064.headers"]}

Test

$ clojure -A:test

License

Copyright © 2020 Michiel Borkent

Distributed under the EPL License, same as Clojure. See LICENSE.

babashka.curl's People

Contributors

borkdude avatar daviddpark avatar jaydeesimon avatar laurio avatar lispyclouds avatar rymndhng avatar sudorock avatar xificurc 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  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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

babashka.curl's Issues

form-params are sent as multipart/form-data

form-params by default should be sent as application/x-www-form-urlencoded but are being sent as multipart/form-data

Ex:

(println (curl/post "https://httpbin.org/post" {:form-params {"test" "should be application/x-www-form-urlencoded"}}))'
{:status 200, :headers {date Fri, 18 Sep 2020 20:26:29 GMT, content-type application/json, content-length 506, server gunicorn/19.9.0, access-control-allow-origin *, access-control-allow-credentials true}, :body {
....
  "form": {
    "test": "should be application/x-www-form-urlencoded"
  },
  "headers": {
...
    "Content-Type": "multipart/form-data; boundary=------------------------8a57aa5eaacd734a",
    "Host": "httpbin.org",
  },
  "url": "https://httpbin.org/post"
}
, :err , :process #object[java.lang.ProcessImpl 0x24087025 Process[pid=62285, exitValue=0]], :exit 0}

I am able to work around this for now using :raw-args

Ex

bb -i '(println (curl/post "https://httpbin.org/post" {:raw-args ["-d" "test=foo"]}))'
{:status 200, :headers {date Fri, 18 Sep 2020 20:30:54 GMT, content-type application/json, content-length 427, server gunicorn/19.9.0, access-control-allow-origin *, access-control-allow-credentials true}, :body {
...
  "form": {
    "test": "foo"
  },
  "headers": {
...
    "Content-Type": "application/x-www-form-urlencoded",
    "Host": "httpbin.org",
  },
...
  "url": "https://httpbin.org/post"
}
, :err , :process #object[java.lang.ProcessImpl 0x63729ca0 Process[pid=64261, exitValue=0]], :exit 0}

Provide an example of the equivalent of curl -F option

First of all, nice project (among other), thanks for sharing it!

I recently had to use curl -H ... -F file=@/path/to/file in a Clojure project, relying on clojure.java.shell for this (see the code here).

Would it make sense to use babashka.curl here (knowing that I need to compile with GraalVM)?

If so could you give an example in the README.md of how it would look like?

This is not to say that the README.md should cover all curl options of course, but it seems documenting this one could be handy.

Let me know if I can help. Thanks!

Segmentation fault when reading /etc/passwd

$ bb '(curl/post "https://google.com")'
Segmentation fault

$ strace bb '(curl/post "http://google.com")'
...
openat(AT_FDCWD, "/etc/passwd", O_RDONLY|O_CLOEXEC) = 5
fstat(5, {st_mode=S_IFREG|0644, st_size=1934, ...}) = 0
lseek(5, 0, SEEK_SET)                   = 0
read(5, "root:x:0:0:root:/root:/bin/bash\n"..., 4096) = 1934
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0xe5} ---
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x702d070} ---
+++ killed by SIGSEGV +++
Segmentation fault

What else should I check?

Set up CI

  • Run clojure -A:test on Github actions
  • Protect master branch

Bundled curl with Windows 10 doesn't support `--compressed`

Using babashka v.0.4.1 on Windows, I tried to run the following in the REPL:

user=> (curl/get  "www.google.com")
clojure.lang.ExceptionInfo: curl: option --compressed: the installed libcurl version doesn't support this
curl: try 'curl --help' for more information
[at <repl>:1:1

Doing this instead it works:

user=> (curl/get "www.google.com" {:compressed false})
[Google webpage output]

We probably need to check for features. Using curl --version on Windows I got:

curl 7.55.1 (Windows) libcurl/7.55.1 WinSSL Release-Date: 2017-11-14, security patched: 2019-11-05
Protocols: dict file ftp ftps http https imap imaps pop3 pop3s smtp smtps telnet tftp
Features: AsynchDNS IPv6 Largefile SSPI Kerberos SPNEGO NTLM SSL 

On my NixOS system:

curl 7.74.0 (x86_64-pc-linux-gnu) libcurl/7.74.0 OpenSSL/1.1.1k zlib/1.2.11 libssh2/1.9.0 nghttp2/1.41.0
Release-Date: 2020-12-09
Protocols: dict file ftp ftps gopher http https imap imaps mqtt pop3 pop3s rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS GSS-API HTTP2 HTTPS-proxy IPv6 Kerberos Largefile libz NTLM NTLM_WB SPNEGO SSL TLS-SRP UnixSockets

According to this bug report, we need to check for either libz or brotli feature.

BREAKING: Always parse response headers and return map

Now that babashka.curl is able to parse headers, it makes sense to always return a map with body and headers. This is a breaking change, but I think for the good of the future of this library. Better break early on and make it easier to not break anything later on.

Unable to send post req or debug

Hey @borkdude , I spent few hour on that and failed to succeed :D, I essentially trying to do the following:

// Upload image path to imgur
fn upload_to_imgur(path string) http.Response {
	server := {
		'client_id': 'ea6c0ef2987808e'
		'url': 'https://api.imgur.com/3/image'
	}
	file := os.read_file(path) or {
		panic(err)
	}
	req := http.FetchConfig{
		method: .post
		headers: {
			'Authorization': 'Client-ID  ea6c0ef2987808e'
			'Content-Type': 'image/png'
			'Connection': 'keep-alive'
		}
		data: base64.encode(file)
	}
	res := http.fetch('https://api.imgur.com/3/image', req) or {
		panic(err)
	}
	return res
}

I tired debugging and using

(curl/post "https://api.imgur.com/3/image"
                  {:headers {"Content-Type" "image/png"
                             "Authorization" "Client-ID ea6c0ef2987808e"
                             "Connection" "keep-alive"}
                   :body (encode (slurp (io/file "/tmp/29-11-2020-03:05:14.png"))) ;; base64 encoded string
                   :throw false
                   :as :stream
                   })

I get back status 400

{:status 400, :headers {"access-control-expose-headers" "X-RateLimit-ClientLimit, X-RateLimit-ClientRemaining, X-RateLimit-UserLimit, X-RateLimit-UserRemaining, X-RateLimit-UserReset, Geo-City, Geo-Region, Geo-Metro-Code, Geo-Country-Code", "access-control-allow-headers" "Authorization, Content-Type, Accept, X-Mashape-Authorization, IMGURPLATFORM, IMGURUIDJAFO, SESSIONCOUNT, IMGURMWBETA, IMGURMWBETAOPTIN", "x-post-rate-limit-remaining" "1223", "server" "nginx", "content-type" "application/json", "access-control-allow-origin" "*", "etag" "\"3b62776d97153806056f652c0b4457f810b39aef\"", "x-post-rate-limit-limit" "1250", "access-control-allow-methods" "GET, PUT, POST, DELETE, OPTIONS", "date" "Sun, 29 Nov 2020 15:22:25 GMT", "x-post-rate-limit-reset" "2314", "cache-control" "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"}, :body #object[java.io.SequenceInputStream 0xa22ae23 "java.io.SequenceInputStream@a22ae23"], :err #object[java.lang.ProcessImpl$ProcessPipeInputStream 0x734d0f1b "java.lang.ProcessImpl$ProcessPipeInputStream@734d0f1b"], :process #object[java.lang.ProcessImpl 0x595f7666 "Process[pid=200065, exitValue=0]"], :exit #delay[{:status :pending, :val nil} 0x687e4cd9]}

If you can point out what I'm not doing right it would be great.

Thanks

Pass --compressed by default.

See test:

(deftest compressed-test
  (is (-> (curl/get "https://api.stackexchange.com/2.2/sites")
          :body (json/parse-string true) :items))
  (is (thrown?
       Exception
       (-> (curl/get "https://api.stackexchange.com/2.2/sites"
                     {:compressed false})
           :body (json/parse-string true) :items))))

Cannot retry when using `:as :stream` option

(require '[babashka.curl :as curl])

;; For context, this takes around 8s and throws error with 500 status, as expected. 
;; This is just an example of how it works normally.
(curl/get "https://httpstat.us/500" {:raw-args ["--retry" "3"] :debug true})

;; #### Repro of bug ######
;; Actual: Takes around 300ms and throws error with 500 status
;; Expected: Should retry 3 times, which would take about 8s total
(curl/get "https://httpstat.us/500" {:as :stream :raw-args ["--retry" "3"] :debug true})

;; For comparison
;; curl -N --retry 3 https://httpstat.us/500
;; retries as expected
;; (-N is the option added here
;; https://github.com/babashka/babashka.curl/blob/e1f33ffa22728553bf2242db5e2a4ca349fab04b/src/babashka/curl.clj#L126 )    

Is there a way to display curl progress information?

This is not really an issue but a question.

The curl CLI tool shows a progress bar when either -# or --progress-bar are passed to it. However, this wrapper library uses --silent, so no progress information is displayed.

As far as I understood, it's not possible to override this behavior using :raw-args. Am I correct?

Resource leak

I wrote a small daemon which monitor's ECS metadata endpoint and reports basic Docker stats via Statsd protocol. So far soo good!
It runs on a 10s interval - so every 10s it will use babashka.curl to poll the endpoint. I've noticed this morning that at some point we stopped receiving metrics from our services.
I've looked into the logs and this line popped up:

:message Cannot run program "curl": error=24, Too many open files"

I looked at the source and it looks like despite calling .delete on the temp file, file handles are not freed. I wrote a simple babashka script which polls a local web server in a very tight loop (1s).
Then checked the file handles opened by the process:


# - ubuntu: lsof -p 4160 
# ...removed garbage....
bb      4160 ubuntu    5r   REG    8,1      477   4657 /var/tmp/babashka.curl2791103685985735359.headers (deleted)
bb      4160 ubuntu    6r   REG    8,1      477   6180 /var/tmp/babashka.curl4321920526964170231.headers (deleted)
bb      4160 ubuntu    7r   REG    8,1      477  10101 /var/tmp/babashka.curl4676676831291833005.headers (deleted)
bb      4160 ubuntu    8r   REG    8,1      477  12710 /var/tmp/babashka.curl9129177778621420130.headers (deleted)
bb      4160 ubuntu    9r   REG    8,1      477  12713 /var/tmp/babashka.curl1755931921803218887.headers (deleted)
bb      4160 ubuntu   10r   REG    8,1      477  12715 /var/tmp/babashka.curl2374590227721063813.headers (deleted)
bb      4160 ubuntu   11r   REG    8,1      477  12716 /var/tmp/babashka.curl7555690216220885691.headers (deleted)
bb      4160 ubuntu   12r   REG    8,1      477  12717 /var/tmp/babashka.curl5783864275205825599.headers (deleted)
bb      4160 ubuntu   13r   REG    8,1      477  12718 /var/tmp/babashka.curl3391626115413234840.headers (deleted)
bb      4160 ubuntu   14r   REG    8,1      477  12719 /var/tmp/babashka.curl3294222859950290307.headers (deleted)
bb      4160 ubuntu   15r   REG    8,1      477  12720 /var/tmp/babashka.curl1858309968829563681.headers (deleted)
bb      4160 ubuntu   16r   REG    8,1      477  12721 /var/tmp/babashka.curl2708258064880582016.headers (deleted)
bb      4160 ubuntu   17r   REG    8,1      477  12722 /var/tmp/babashka.curl4878472749755100471.headers (deleted)
bb      4160 ubuntu   18r   REG    8,1      477  12723 /var/tmp/babashka.curl3797198481882795669.headers (deleted)
bb      4160 ubuntu   19r   REG    8,1      477  12724 /var/tmp/babashka.curl5798938650515700686.headers (deleted)
bb      4160 ubuntu   20r   REG    8,1      477  12725 /var/tmp/babashka.curl8926284960367711330.headers (deleted)
bb      4160 ubuntu   21r   REG    8,1      477  12726 /var/tmp/babashka.curl2296622041669157621.headers (deleted)
bb      4160 ubuntu   22r   REG    8,1      477  12727 /var/tmp/babashka.curl3712698982776823011.headers (deleted)
bb      4160 ubuntu   23r   REG    8,1      477  12728 /var/tmp/babashka.curl2307481706378348242.headers (deleted)
bb      4160 ubuntu   24r   REG    8,1      477  13300 /var/tmp/babashka.curl1480999808202088278.headers (deleted)
bb      4160 ubuntu   25r   REG    8,1      477  15667 /var/tmp/babashka.curl6417597933549367042.headers (deleted)
bb      4160 ubuntu   26r   REG    8,1      477  15668 /var/tmp/babashka.curl961934724018028790.headers (deleted)
bb      4160 ubuntu   27r   REG    8,1      477  15670 /var/tmp/babashka.curl809665755915233747.headers (deleted)
bb      4160 ubuntu   28r   REG    8,1      477  15671 /var/tmp/babashka.curl241764706011866924.headers (deleted)
bb      4160 ubuntu   29r   REG    8,1      477  15672 /var/tmp/babashka.curl3198063764130376943.headers (deleted)
bb      4160 ubuntu   30r   REG    8,1      477  15673 /var/tmp/babashka.curl2395939605882077064.headers (deleted)
bb      4160 ubuntu   31r   REG    8,1      477  15674 /var/tmp/babashka.curl526193298524954950.headers (deleted)

#....

#   - ubuntu : lsof -p 4160 2>/dev/null | grep headers | wc -l
510

Of course on exit all file handles are freed - not sure what the fix is, as the Java doc for File doesn't indicate any other way of freeing the handles.

Capture standard error from curl shell command

I'm interested in capturing the standard error and doing something with it. For example, when setting a timeout {:raw-args ["--max-time" "30"]}. Full example:

user=> (curl/get "https://nytimes.com" {:raw-args ["--max-time" "0.01"]})
curl: (7) Failed to connect to nytimes.com port 443: Operation timed out
{:status nil, :headers {}, :body "", :process #object[java.lang.UNIXProcess 0x3ed14362 "java.lang.UNIXProcess@107e169f8"]}
user=> (-> *1 :process (.getErrorStream) slurp)
""

It looks like the main reason it's empty is because the process's standard error destination is being set to ProcessBuilder$Redirect/INHERIT here and therefore not present in the error input stream.

The lowest hang fruit thing I can think of is to remove that line so that the default destination is ProcessBuilder$Redirect/PIPE but maybe there's a reason it's set to ProcessBuilder$Redirect/INHERIT?

Beyond that, there's probably better ways to get at the fact that something went wrong and curl returned an error besides manually pulling it from the error input stream (for example, adding it to the result map with a curl error key or throw an exception with details in an ExceptionInfo object).

Let me know what you think. I'd be happy to work on a PR.

-Jeff

curl can print multiple headers after HTTP 100 Continue

That makes this response be a little weird:

$ bb '(curl/post "https://postman-echo.com/post" {:form-params {"name" "Michiel Borkent"}})'
{:status 100, :headers {}, :body "HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=utf-8\r\nDate: Fri, 10 Apr 2020 09:48:54 GMT\r\nETag: W/\"168-/v8pA7AIbIiauhH8I7o39mwsHSI\"\r\nServer: nginx\r\nset-cookie: sails.sid=s%3AjKHSyn-zpkN7fbBa4RnafRH2ppDm2xgt.qkIUYZ47arNyc4J%2Fi4FK0MZTBa9mBuE3w4KUilN2imY; Path=/; HttpOnly\r\nVary: Accept-Encoding\r\nContent-Length: 360\r\nConnection: keep-alive\r\n\r\n{\"args\":{},\"data\":{},\"files\":{},\"form\":{\"name\":\"Michiel Borkent\"},\"headers\":{\"x-forwarded-proto\":\"https\",\"host\":\"postman-echo.com\",\"content-length\":\"154\",\"accept\":\"*/*\",\"content-type\":\"multipart/form-data; boundary=------------------------0922737590b9efdf\",\"user-agent\":\"curl/7.54.0\",\"x-forwarded-port\":\"443\"},\"json\":null,\"url\":\"https://postman-echo.com/post\"}"}

Downloading binaries

@sogaiu tried to download a binary as follows, which didn't work:

(spit "foo.zip" (curl/get ...))

This might be because the bits get converted to UTF-8 or something along the way

For downloading binary files one has to use the :raw-args option with ["-o" "filename.zip"]. Maybe it would be nice to support this as a proper option (called :out?).
We could also implement an option that returns the raw stream: {:as :stream}

As well as --data-raw, add --data-binary

According to the curl man page,

--data-raw is almost the same but does not have a special interpretation of the @ character. To post data purely binary, you should instead use the --data-binary option.

Currently, the --data-raw option does not work with java.io.File values. In my case, I'm trying to upload a binary file to the server, and want to avoid -d being generated in the curl command because it seems to corrupt files (e.g. CRs and newlines are stripped out).

Add documentation for data-raw

I've needed to use the following syntax

(curl/put address
          {:data-raw {"foo" ["bar" "baz"]}
           :throw false
           :debug true
           :raw-args ["-k"]})

So it'd be good to add some examples to the README as well

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.