Giter Site home page Giter Site logo

ikea-api-client's Introduction

Client for several IKEA APIs.

Test Python Downloads

Features

With this library you can access following IKEA's APIs:

  • Cart,
  • Home Delivery and Collect Services (actually, Order Capture),
  • Items info (2 services),
  • 3D models,
  • Purchases (history and order info),
  • Search,
  • Stock.

Also the package:

  • Is backend agnostic: choose HTTP library you want (async, too!),
  • Is fully typed and tested,
  • Has optional wrappers around some APIs based on Pydantic.

Installation

pip install ikea_api
  • Use httpx — awesome async HTTP library — as backend:
pip install "ikea_api[httpx]"
pip install "ikea_api[requests]"
  • Use wrappers:
pip install "ikea_api[wrappers]"
  • Install everything:
pip install "ikea_api[all]"

Usage

ikea_api is unusual API client. It decouples I/O from logic for easier testing and maintenance. As a bonus, you can use literally any HTTP library. Let's have a look at how to work with ikea_api.

import ikea_api

# Constants like country, language, base url
constants = ikea_api.Constants(country="us", language="en")
# Search API
search = ikea_api.Search(constants)
# Search endpoint with prepared data
endpoint = search.search("Billy")

As you can see, nothing happened up to this point. Code suggests that we already should get the result of the search but we don't. What happened is search() returned a data class that contains information about endpoint that can be interpreted by an endpoint runner. There are two built-in: for requests (sync) and httpx (async), but you can easily write one yourself.

Here's how you would use requests one:

ikea_api.run(endpoint)

And httpx one:

await ikea_api.run_async(endpoint)

ikea_api.run_async() is async function, so you have to "await" it or run using asyncio.run().

Endpoints reference

🔑 Authorization

First time you open ikea.com, guest token is being generated and stored in cookies. Same thing must be done in here before using any endpoint.

This token expires in 30 days.

ikea_api.Auth(constants).get_guest_token()

Previously you could login as user (with login and password), but now there's very advanced telemetry that I wouldn't be able to solve in hundred years 🤪

🛒 Cart

With this endpoint you can do everything you can using IKEA's frontend:

cart = ikea_api.Cart(constants, token=...)
  • Show the cart
cart.show()
  • Clear it
cart.clear()
  • Add, update and delete items
cart.add_items({"30457903": 1})  # { item_code: quantity }

cart.update_items({"30457903": 5})

cart.remove_items(["30457903"])
  • Set and clear coupon
cart.set_coupon(...)

cart.clear_coupon()
  • and even copy another user's cart.
cart.copy_items(source_user_id=...)

You can edit your user's actual cart if you use authorized token (copy-paste from cookies).

💡 There's wrapper that clears current cart and adds items with error handling: if requested item doesn't exist, the function just skips it and tries again.

ikea_api.add_items_to_cart(  # Function returns items that can't be added. In this case: ['11111111']
    cart=cart,
    items={
        "30457903": 1,
        "11111111": 2,  # invalid item that will be skipped
    },
)

🚛 Order Capture

Check pickup or delivery availability. If you need to know whether items are available in stores, use Item availability endpoint or ikea-availability-checker.

order = ikea_api.OrderCapture(constants, token=token)

cart_show = run(cart.show())
items = ikea_api.convert_cart_to_checkout_items(cart_show)

checkout_id = run(order.get_checkout(items))
service_area_id = run(
    order.get_service_area(
        checkout_id,
        zip_code="02215",
        state_code="MA",  # pass State Code only if your country has them
    )
)
home_services = run(order.get_home_delivery_services(checkout_id, service_area_id))
collect_services = run(
    order.get_collect_delivery_services(checkout_id, service_area_id)
)

💡 You can use wrapper to add items to cart (clearing cart before and handling unknown item errors if they appear) and parse response in nice Pydantic models:

services = await ikea_api.get_delivery_services(
    constants=constants,
    token=...,
    items={
        "30457903": 1,
        "11111111": 2,  # invalid item that will be skipped
    },
    zip_code="101000",
)
services.delivery_options  # List of parsed delivery services
services.cannot_add  # ['11111111']

📦 Purchases

purchases = ikea_api.Purchases(constants, token=token)

History

This method requires authentication, so if you don't have authorized token, it won't work.

purchases.history()

# Get all purchases:
purchases.history(take=10000)

# Pagination:
purchases.history(take=10, skip=1)

💡 Get parsed response with the wrapper:

ikea_api.get_purchase_history(purchases)  # Returns a list of parsed purchases

Order info

purchases.order_info(order_number=..., email=...)

# If you have authorized token, you can drop email:
purchases.order_info(order_number="111111111")

# The method also has other params but they're mostly internal:
purchases.order_info(
    order_number=...,
    email=...,
    queries=...,  # Queries that will be included in request, combine any of: ["StatusBannerOrder", "CostsOrder", "ProductListOrder"]. By default, all of them are included.
    # Params below are relevant to ProductListOrder
    skip_products=...,
    skip_product_prices=...,
    take_products=...,
)

💡 Get parsed response with the wrapper:

ikea_api.get_purchase_info(  # Returns parsed purchase object. Items are not listed.
   purchases=purchases,
   order_number=...,
   email=...,
)

🪑 Item info

Get item specification by item code (product number or whatever). There are 2 endpoints to do this because you can't get all the data about all the items using only one endpoint.

ingka_items = ikea_api.IngkaItems(constants)
ingka_items.get_items(["30457903"])

pip_item = ikea_api.PipItem(constants)
pip_item.get_item("30457903")

📦 Item 3D models

Get 3D models by item code.

rotera_item = ikea_api.RoteraItem(constants)
rotera_item.get_item("30221043")

🟢 Item availability

Get availability by item code (product number or whatever).

stock = ikea_api.Stock(constants)
stock.get_stock("30457903")

🔎 Search

Search for products in the product catalog by product name. Optionally also specify a maximum amount of returned search results (defaults to 24) and types of required search results.

search = ikea_api.Search(constants)
search.search("Billy")

# Retrieve 10 search results (default is 24)
search.search("Billy", limit=10)

# Configure search results types
search.search(
    "Billy",
    types=...,  # Combine any of: ["PRODUCT", "CONTENT", "PLANNER", "REFINED_SEARCHES", "ANSWER"]
)

🛠 Utilities

Parse item codes

Parse item codes from string or list.

assert ikea_api.parse_item_codes("111.111.11") == ["11111111"]
assert ikea_api.parse_item_codes("11111111, 222.222.22") == ["11111111", "22222222"]
assert ikea_api.parse_item_codes("111") == []

Format item code

Parse item code and format it.

assert ikea_api.format_item_code("11111111") == "111.111.11"
assert ikea_api.format_item_code("111-111-11") == "111.111.11"
assert ikea_api.format_item_code("111.111.11") == "111.111.11"

ikea-api-client's People

Contributors

actions-user avatar dependabot[bot] avatar erik142 avatar github-actions[bot] avatar letmaik avatar momoson avatar sqr avatar vrslev 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

Watchers

 avatar  avatar  avatar  avatar  avatar

ikea-api-client's Issues

API endpoint IngkaItems does not work as expected

When trying to fetch info on some IKEA items, I ran into the following error message being raised:

File "foo/bar.py", line 19, in <module>
  response = ikea_api.IngkaItems()(['40263848'])
File "/usr/local/lib/python3.9/site-packages/ikea_api/_endpoints/item_ingka.py", line 39, in __call__
  return self._get(params={"itemNos": item_codes})
File "/usr/local/lib/python3.9/site-packages/ikea_api/_api.py", line 60, in _get
  return self._handle_response(response)
File "/usr/local/lib/python3.9/site-packages/ikea_api/_api.py", line 44, in _handle_response
  self._error_handler(response)
File "/usr/local/lib/python3.9/site-packages/ikea_api/_endpoints/item_ingka.py", line 34, in _error_handler
  raise ItemFetchError(response, item_codes)
ikea_api.exceptions.ItemFetchError: (400, '{"error":{"code":400,"message":"invalid GetItemCommunicationsRequest.ClassUnitType: value is not a valid enum"}}')

I have tested this with multiple item numbers.

KeyError: 'details' when using US zip codes

Hello, I am investigating the following error:

Traceback (most recent call last):
  File "ikea.py", line 17, in <module>
    api.OrderCapture(zip_code="02215")
  File "/home/square/ikea-api/ikea_api/core.py", line 64, in OrderCapture
    return OrderCapture(self._token, zip_code)()
  File "/home/square/ikea-api/ikea_api/endpoints/order_capture/__init__.py", line 29, in __call__
    return self.get_delivery_services()
  File "/home/square/ikea-api/ikea_api/endpoints/order_capture/__init__.py", line 93, in get_delivery_services
    delivery_area = self._get_delivery_area(checkout)
  File "/home/square/ikea-api/ikea_api/endpoints/order_capture/__init__.py", line 80, in _get_delivery_area
    response = self._call_api(
  File "/home/square/ikea-api/ikea_api/api.py", line 70, in _call_api
    self._error_handler(response.status_code, response_dict)
  File "/home/square/ikea-api/ikea_api/endpoints/order_capture/__init__.py", line 33, in _error_handler
    raise OrderCaptureError(response)
  File "/home/square/ikea-api/ikea_api/errors.py", line 44, in __init__
    if response["message"] == response["details"]
KeyError: 'details'

It seems to happen with all the United States zip codes I have tried.

My script is as follows:

from ikea_api import IkeaApi

api = IkeaApi(
    token=...,  # If you already have a token and stored it somewhere
    country_code="us",
    language_code="en",
)

api.login_as_guest()

cart = api.Cart
cart.add_items({"90446761": 1})
# print(cart.show())

api.OrderCapture(zip_code="02215")

Showing the cart works.

I am going to query the api manually and see how the response looks, in case I can spot the problem.

Purchases API has changed

Currently you get an Internal Server Error when using the Purchases API.
I already found that the API endpoint changed
ikea_api/endpoints/purchases.py > _get_session_info() must now use
https://cssom-prod.ingka.com/purchase-history/graphql

However I still get this JSON response:

{
    "data": {
        "history": []
    }
}

When I use a browser I however am able to see my history.
From the network tab it appears that there is another request made before the one that returns the history
First one:
Request:

[
  {
    "operationName": "Authenticated",
    "query": "query Authenticated {\n  authenticated\n}",
    "variables": {}
  }
]

Response:

[
  {
    "data": {
      "authenticated": true
    }
  }
]

Second one:
Request:

[
  {
    "operationName": "History",
    "query": "query History($skip: Int!, $take: Int!) {\n  history(skip: $skip, take: $take) {\n    id\n    dateAndTime {\n      ...DateAndTime\n      __typename\n    }\n    status\n    eventStatus\n    storeName\n    totalCost {\n      code\n      value\n      formatted\n      __typename\n    }\n    type\n    __typename\n  }\n}\n\nfragment DateAndTime on DateAndTime {\n  time\n  date\n  formattedLocal\n  formattedShortDate\n  formattedLongDate\n  formattedShortDateTime\n  formattedLongDateTime\n  __typename\n}",
    "variables": {
      "skip": 0,
      "take": 5
    }
  }
]

Response:

[
  {
    "data": {
      "history": [
        {
          "id": "100",
          "status": "IN_PROGRESS"
          "more fields": "..."
          
        },
        {
          "id": "101",
          "status": "COMPLETED"
          "more fields": "..."
          
        }
      ]
    }
  }
]

I'm not too familiar with these requests yet and couldn't figure out how to fix it :/

KeyError: 'details' when using Swedish OrderCapture API

Hello,

Thank you for creating this IKEA api client, I find it very useful for a project that I am currently working on. I am having an issue with the "order capture" api in Sweden, which seems to result in the same error as #33. The latest version (0.7.0) does not solve this issue for me. See error below:

Traceback (most recent call last):
  File "d:\Filer\Utveckling\Projekt\Python\IKEA-notifier\src\app.py", line 8, in <module>
  File "d:\Filer\Utveckling\Projekt\Python\IKEA-notifier\src\api\ikea_availability_api.py", line 17, in getAvailability
    availabilities = self.__IKEA_API.OrderCapture(
  File "C:\Users\Erik\AppData\Local\Programs\Python\Python39\lib\site-packages\ikea_api\core.py", line 64, in OrderCapture
    return OrderCapture(self._token, zip_code, state_code)()
  File "C:\Users\Erik\AppData\Local\Programs\Python\Python39\lib\site-packages\ikea_api\endpoints\order_capture\__init__.py", line 32, in __call__
    return self.get_delivery_services()
  File "C:\Users\Erik\AppData\Local\Programs\Python\Python39\lib\site-packages\ikea_api\endpoints\order_capture\__init__.py", line 97, in get_delivery_services
    checkout: str | None = self._get_checkout()
  File "C:\Users\Erik\AppData\Local\Programs\Python\Python39\lib\site-packages\ikea_api\endpoints\order_capture\__init__.py", line 71, in _get_checkout
    response: dict[str, str] = self._call_api(
  File "C:\Users\Erik\AppData\Local\Programs\Python\Python39\lib\site-packages\ikea_api\api.py", line 70, in _call_api
    self._error_handler(response.status_code, response_dict)
  File "C:\Users\Erik\AppData\Local\Programs\Python\Python39\lib\site-packages\ikea_api\endpoints\order_capture\__init__.py", line 36, in _error_handler
    raise OrderCaptureError(response)
    if response["message"] == response["details"]
KeyError: 'details'

The error can be reproduced by running the following code:

api = IkeaApi(country_code='se', language_code='sv')
api.login_as_guest()

api.Cart.clear()
api.Cart.add_items({"20512245": 1})
cart_show = api.Cart.show()    # The show method works as expected, showing the added item in the cart
api.OrderCapture(zip_code="12345")    # Mock zip-code here, but it does not work with a valid zip code either

I have debugged the issue myself and have traced it down to the hard coded "ru" language code in the file "src/ikea_api/endpoints/order_capture/__init__.py" (line 66):

data = {
"shoppingType": "ONLINE",
"channel": "WEBAPP",
"checkoutType": "STANDARD",
"languageCode": "ru",
"items": items,
"deliveryArea": None,
}

By changing this to:

data = {
            "shoppingType": "ONLINE",
            "channel": "WEBAPP",
            "checkoutType": "STANDARD",
            "languageCode": Constants.LANGUAGE_CODE,
            "items": items,
            "deliveryArea": None,
        }

the issue is solved for me. I do not think that this change would create any further issues for other regions and languages, so I will go ahead and create a PR to fix this.

If there is any potential issue with this change, please let me know.

Thanks!

Retail ID is not supported: en

Hello, thanks for taking the time to make this client.

The examples provided in the readme work, however, when I try to change the language code to english and location to the united states, I get the following error

ikea_api.errors.UnauthorizedError: Retail ID is not supported: en

Traceback (most recent call last): File "ikea.py", line 14, in <module> cart.add_items({"40453645": 1}) File "/home/square/ikea-api/ikea_api/endpoints/cart/__init__.py", line 21, in inner return self._call_api(data=self._build_payload(query, **variables)) # type: ignore File "/home/square/ikea-api/ikea_api/api.py", line 69, in _call_api self._basic_error_handler(response.status_code, response_dict) File "/home/square/ikea-api/ikea_api/api.py", line 43, in _basic_error_handler raise UnauthorizedError(response) ikea_api.errors.UnauthorizedError: Retail ID is not supported: en

The only thing I've changed is

country_code="us", language_code="en",

Any ideas on why this might be causing an issue?

Thanks!

Use different query for adding items

{"query":"\n    mutation {\n      addItems(items: [\n        {itemNo: \"40372084\", quantity: 1}\n      ]) {\n        quantity\n        context {\n          userId\n          isAnonymous\n          retailId\n        }\n      }\n    }\n  "}

got output errors on everything

Hi there, does this API still works?

I have errors every time I try to get info on an item:

EndpointInfo(func=functools.partial(<function Stock.get_stock at 0x104a972e0>, <ikea_api.endpoints.stock.Stock object at 0x1061d5a60>, '30457903'), handlers=[<function handle_json_decode_error at 0x104a82f20>, <function handle_graphql_error at 0x104a94360>])

feature suggestion: Search for products by name

Hi again,

I believe that it would be beneficial to be able to search for products by name and get information back, including the item number for the product. I have a first implementation ready for this feature if it is of any interest to this project?

I have only tested it on the Swedish version of the corresponding IKEA API. However, the API seems to follow the same "region naming scheme" as the existing API endpoints in this project, so I believe that it should work in the same way for other regions. Let me know if this sounds interesting. If so, I'll add a PR for this 😄

Only able to get_items from Russia

If I change the COUNTRY_CODE and LANGUAGE_CODE from "ru" to "hu" or "at" it's not working anymore. With default values it works fine and I get a response.

If source is:

from ikea_api.wrappers import get_items
from ikea_api._constants import Constants

Constants.COUNTRY_CODE = "ru"
Constants.LANGUAGE_CODE = "ru"

print(get_items(["09251777"]))

Response is:

[ParsedItem(is_combination=True, item_code='49251780', name='КЛИППАН, 2-местный диван, Кабуса темно-серый', image_url='https://www.ikea.com/ru/ru/images/products/klippan-2-mestnyy-divan-kabusa-temno-seryy__0562986_pe663639_s5.jpg', weight=0.0, child_items=[ChildItem(name=None, item_code='50501293', weight=0.0, qty=1), ChildItem(name=None, item_code='00398726', weight=0.0, qty=1)], price=14999, url=HttpUrl('https://www.ikea.com/ru/ru/p/klippan-klippan-2-mestnyy-divan-kabusa-temno-seryy-s49251780/', scheme='https', host='www.ikea.com', tld='com', host_type='domain', port='443', path='/ru/ru/p/klippan-klippan-2-mestnyy-divan-kabusa-temno-seryy-s49251780/'), category_name='Двухместные диваны', category_url=HttpUrl('https://www.ikea.com/ru/ru/cat/dvuhmestnye-divany-10668/', scheme='https', host='www.ikea.com', tld='com', host_type='domain', port='443', path='/ru/ru/cat/dvuhmestnye-divany-10668/'))]

If source is:

from ikea_api.wrappers import get_items
from ikea_api._constants import Constants

Constants.COUNTRY_CODE = "hu"
Constants.LANGUAGE_CODE = "hu"

print(get_items(["09251777"]))

Response is:

Traceback (most recent call last):
  File "/Users/gergelymarosi/_hobbi/ikea/main.py", line 7, in <module>
    print(get_items(["09251777"]))
  File "/Users/gergelymarosi/_hobbi/ikea/venv/lib/python3.9/site-packages/ikea_api/wrappers/__init__.py", line 192, in get_items
    fetched_items_ingka_pip = _get_ingka_pip_items(pending_item_codes)
  File "/Users/gergelymarosi/_hobbi/ikea/venv/lib/python3.9/site-packages/ikea_api/wrappers/__init__.py", line 164, in _get_ingka_pip_items
    ingka_items = _get_ingka_items(item_codes)
  File "/Users/gergelymarosi/_hobbi/ikea/venv/lib/python3.9/site-packages/ikea_api/wrappers/__init__.py", line 142, in _get_ingka_items
    responses.append(fetcher(item_codes_chunk))
  File "/Users/gergelymarosi/_hobbi/ikea/venv/lib/python3.9/site-packages/ikea_api/_endpoints/item_ingka.py", line 39, in __call__
    return self._get(params={"itemNos": item_codes})
  File "/Users/gergelymarosi/_hobbi/ikea/venv/lib/python3.9/site-packages/ikea_api/_api.py", line 60, in _get
    return self._handle_response(response)
  File "/Users/gergelymarosi/_hobbi/ikea/venv/lib/python3.9/site-packages/ikea_api/_api.py", line 44, in _handle_response
    self._error_handler(response)
  File "/Users/gergelymarosi/_hobbi/ikea/venv/lib/python3.9/site-packages/ikea_api/_endpoints/item_ingka.py", line 34, in _error_handler
    raise ItemFetchError(response, item_codes)
ikea_api.exceptions.ItemFetchError: (400, '{"error":{"code":400,"message":"invalid GetItemCommunicationsRequest.ClassUnitType: value is not a valid enum"}}')

Handle internal error

ikea_api/api.py», line 62, in basic_error_handler
    raise Exception(code + ‘, ‘ + str(err))
Exception: INTERNAL_ERROR, {‘message’: ‘an internal error has occurred’, ‘extensions’: {‘code’: ‘INTERNAL_ERROR’, ‘requestId’: ‘localhost/ByzaAoxjs4-5483209’}}

Unable to check stock of an item, response includes code 606.

Hi, I've been trying to use your API to check stock of an item, to do this I used the following code:

import ikea_api

constants = ikea_api.Constants(country="pl", language="pl")
stock = ikea_api.Stock(constants)
endpoint = stock.get_stock("00366301")

print(ikea_api.run(endpoint))

This prints out the following message:

{'code': 606, 'message': 'classUnitType in path should be one of [ru sto]'}

And I'm stuck right here, I have no idea what can I do to fix this issue.
Is there anything I can do to fix this? Thanks for reading : )

RCRalph

Using getitems async

@vrslev I debugged my issue from my previous request and it came down to calling ikea_api.get_items in an async manner.

Could I make a request to add to the documentation an example of this to avoid RuntimeError: Event loop is closed

About favorites

Is there any way to add "shopping list" (favorites) functionality? Does IKEA provide access to them in its api, and can you add this to your client?

How to get ITEMS number that has 3D model?

Hello,

I'm attempting to utilize this function:

rotera_item.get_item("30221043")

It works perfectly until I switch to another item ID.

IE when I search for a sofa and retrieve the item number:

{'itemNo': '19319220'}

I keep encountering the following error:

ikea_api.exceptions.ItemFetchError: (404, 'Not Found')

HELP!

Error when trying to get auth token

Hello,

Thanks a lot for providing us with this public repo.

I'm running python 3.11 and my intention is to get product id's and the end goal is serve this repo over a restful api mainly to get product info such as price. I'm getting the below error.

I'm running the below command

ikea_api.Auth(constants).get_guest_token()

and getting the below error

EndpointInfo(func=functools.partial(<function Auth.get_guest_token at 0x1046c6480>, <ikea_api.endpoints.auth.Auth object at 0x1063c33d0>), handlers=[<function handle_json_decode_error at 0x104467c40>, <function handle_401 at 0x1046c5300>, <function handle_not_success at 0x1046c6020>])

400: Invalid GetItemCommunicationsRequest.ClassUnitType: value is not a valid enum

Hey,

I intend to use this library as a backend and write a fancy frontend on it. I made an api layer using flask to call the library but I encountered some issues.

Calling the getitem API results in the following repsonse:
ikea_api.exceptions.NotSuccessError: (400, '{"error":{"code":400,"message":"invalid GetItemCommunicationsRequest.ClassUnitType: value is not a valid enum"}}')

Code snippet:
constants = ikea_api.Constants(country="it", language="it", base_url="https://www.ikea.com") itemCode = ["00366301","29902933","60366303"] items = await ikea_api.get_items(constants,itemCode)

My question is do I ignore the 400?
I am hoping someone managed to get around this.

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.