Code Monkey home page Code Monkey logo

duffel-api-python's Introduction

PyPI Supported Python versions Build Status Code style:black Downloads

duffel-api

Python client library for the Duffel API.

Requirements

  • Python 3.8+

Getting started

pip install duffel-api

Usage

You first need to set the API token you can find in the Duffel dashboard under the section Developers > Access Tokens.

Once you have the token, you can call Duffel() with the value:

from duffel_api import Duffel

access_token = 'test_...'
client = Duffel(access_token = access_token)

After you have a client you can interact with, you can make calls to the Duffel API:

from duffel_api import Duffel

client = Duffel(access_token = 'test...')

offer_requests = client.offer_requests.list()
for offer_request in offer_requests:
    print(offer_request.id)

You can find a complete example of booking a flight in ./examples/book-flight.py.

Development

Testing

Run all the tests:

tox

As part of running tox, a code coverage report is built for you. You can navigate it by opening htmlcov/index.html in a browser, or if in a OS that supports it by using open (alternative xdg-open):

open ./htmlcov/index.html

Packaging

Setup pypi config (~/.pypirc):

[pypi]
  username = __token__
  password = pypi-generated-token

[testpypi]
  username = __token__
  password = pypi-generated-token

Install dependencies:

pip install wheel twine

Build the package before uploading:

python setup.py sdist bdist_wheel

Upload packages (test):

twine upload -r testpypi --verbose dist/*

The above will upload the packages to test.pypi.org which will allow you to verify all is well with your upload before uploading it to the main pypi repository.

twine upload -r pypi --verbose dist/*

duffel-api-python's People

Contributors

cianyleow avatar dependabot[bot] avatar jameswair avatar jesse-c avatar jonfinerty avatar nlopes avatar sgerrand avatar

Stargazers

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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

duffel-api-python's Issues

Make similar exceptions use the same params

Describe the bug
Here passes through the empty list of passengers, but here passes through the number (0) - but both are InvalidNumberOfX errors.

To Reproduce
n/a

Expected behavior
It’d be good to get some consistency around what’s returned in similar Exception types.

System (please complete the following information):
n/a

Additional context
n/a

Passenger Type Bug

Hi i got error in passengers model

from ..models import LoyaltyProgrammeAccount

class Passenger:
"""The passenger travelling"""

please add here infant_without_seat because when i search with child with age 1 in passenger response i got infant_without_seat type and code got Exception
allowed_types = ["adult"]

class InvalidType(Exception):
    """Invalid passenger type provided"""

def __init__(self, json):
    for key in json:
        value = json[key]
        if key == "type" and value and value not in Passenger.allowed_types:
            raise Passenger.InvalidType(value)
        if key == "loyalty_programme_accounts":
            value = [
                LoyaltyProgrammeAccount(loyalty_programme_account)
                for loyalty_programme_account in value
            ]

        setattr(self, key, value)

invalid_request_error: Not found: The resource you are trying to access does not exist

Describe the bug

When attempting to get an offer by its id CLIENT.offers.get(offer_id) I get this response:

invalid_request_error: Not found: The resource you are trying to access does not exist

This is when using both a test key and a live key

To Reproduce
Steps to reproduce the behavior:

  1. Go to '...'
  2. Click on '....'
  3. Scroll down to '....'
  4. See error

Expected behavior
A clear and concise description of what you expected to happen.

System (please complete the following information):

  • OS: [e.g. Ubuntu]
  • Library version: [e.g. 0.2.0]
  • Python Version: [e.g. 3.10]

Additional context
Add any other context about the problem here.

Avoid creating unnecessary objects by using generator expressions

Describe the bug
In some places, objects are created unnecessarily - e.g. here:

if set(payment.keys()) != set(["amount", "currency", "type"]):

To Reproduce
n/a

Expected behavior
There are slight clarity, performance and memory gains by avoiding unnecessary creation via use of generator expressions - e.g. the above could be changed to:

if not all(k in ["amount", "currency", "type"] for k in payments.keys()):

which only creates a single list, rather than two sets and a list.

System (please complete the following information):
n/a

Additional context
Over such small sizes of list/set it won't make a tangible performance impact, so this is pretty low-priority - but I think it also makes the code a bit clearer.

Allow pagination to be auto or manual

Hello
i got a problem in duffel.offers.list() call because it take almost 11 sec to give data. i explain the scenario first user create offer request with all required data with this api ( duffel.offer_request.execute() without return_offer() ) so this call take 3 to 7 sec and after that i go to the next page with offer ID and make another request to get offers with ( duffel.offers.list() ) and this call take 9 to 12 sec because they got all offers at once with pagination which i cannot control so please give control in pagination in api call because pagination model call api recursively and its stop when all offer they get thank you i hope you understand what i mean.

Improve validation strictness

Describe the bug
Some validation doesn’t seem strict enough - e.g. here doesn’t check the types or allowed values passed in for currency and amount.

To Reproduce
n/a

Expected behavior
Nested data should be validated throughout all levels of the hierarchy.

System (please complete the following information):
n/a

Additional context
Could also fix this issue at the same time!

OfferSliceSegment class doesn't have stops

Describe the bug
In the OfferSliceSegment there is no variable for the stops, so when doing a search of offers there is no possibility to display them (in the API they are available).

class OfferSliceSegment:
    """The segments - that is, specific flights - that the airline is offering
    to get the passengers from the `origin` to the `destination`
    """

    id: str
    aircraft: Optional[Aircraft]
    arriving_at: datetime
    departing_at: datetime
    destination: Airport
    destination_terminal: Optional[str]
    origin: Airport
    origin_terminal: Optional[str]
    distance: Optional[str]
    duration: Optional[str]
    marketing_carrier: Airline
    marketing_carrier_flight_number: str
    operating_carrier: Airline
    operating_carrier_flight_number: Optional[str]
    passengers: Sequence[OfferSliceSegmentPassenger]

To Reproduce
Steps to reproduce the behavior:

  1. Make a search from DXB to AMS (as in the documentation).
  2. Print the offer_resquest.offers and you'll see that there is no stops (where there should be, even if not should be an empty array).
  3. In the class OfferSliceSegment there is no reference to stops.

Expected behavior
It is expected that would be the existance of a List of Object that contains the info of the stops.

System

  • OS: Windoes with a Docker Dev Container with image "mcr.microsoft.com/devcontainers/python:1-3.10-bookworm"
  • Library version: 0.6.0
  • Python Version: 3.10

Suggestion: use of an async request framework (as replacement for `requests`)

Is your feature request related to a problem? Please describe.
The issue is that many (most?) modern python web frameworks are working in an asynchronous fashion (ASGI). The current API you are offering is purely synchronous which make it
"impossible" to use with such frameworks.

Describe the solution you'd like
Most of the time I'm using aiohttp, its interface is very close to requests which makes it a good candidate.

Describe alternatives you've considered
I'm currently re-writing it for myself.

Remark

Also, that's a bit related so I will answer this remark from your code:

# duffel_api/client.py
class Duffel:
    """Client to the entire API"""

    def __init__(self, **kwargs):
        # TODO(nlopes): I really don't like how I've built this- we shouldn't keep
        # instantiating the HttpClient through class inheritance.
        # We should (maybe!) have a singleton pattern on HttpClient and use
        # composition in all of these instead.

        # Keep this as we use it when doing the lazy-evaluation of the different
        # clients
        self._kwargs = kwargs

I think it's not too much of a problem, the real problem is underlying. You are creating one session per init (and therefore per inherited class), the code looks a bit like this:

# duffel_api/http_client.py
class HttpClient:
    def __init__(self, access_token=None, api_url=None, api_version=None, **settings):
        self.http_session = Session()

I think you should spawn a single HTTP session for all APIs (unless there is a good reason not to). Then, you could use that unique session in all subclasses (passing it as argument to the HTTpClient). You don't even need to have a singleton for that an can simply use a lazy property directly in Duffel client, like so (the code is already using aiohttp because I've already rewrote this part):

# duffel_api/client.py
class Duffel:
    _session: Optional[aiohttp.ClientSession] = None

    def __init__(self, **kwargs):
        self._kwargs = kwargs
        self._session = None

    @property
    def http_session(self):
        if self._session is None:
            self._session = aiohttp.ClientSession()
        return self._session

    @lazy_property
    def aircraft(self):
        """Aircraft API - /air/aircraft"""
        return AircraftClient(**self._kwargs, session=self.http_session)
# duffel_api/http_client.py
class HttpClient:
    _session: aiohttp.ClientSession

    URL = "https://api.duffel.com"
    VERSION = "v1"

    def __init__(
        self,
        *,
        session: aiohttp.ClientSession,
        access_token: str = None,
        api_url: str = None,
        api_version: str = None,
        **settings,
    ):
        self._api_url = api_url or HttpClient.URL
        self._api_version = api_version or HttpClient.VERSION

        if not access_token:
            access_token = os.getenv("DUFFEL_ACCESS_TOKEN")
            if not access_token:
                raise ClientError("must set DUFFEL_ACCESS_TOKEN")

        self._headers = {
            "Authorization": f"Bearer {access_token}",
            "User-Agent": f"Duffel/{self._api_version} duffel_api_python/{version()}",
            "Accept": "application/json",
            "Duffel-Version": self._api_version,
        }
        self._settings = settings
        self.http_session = session

    async def _http_call(self, endpoint, method, query_params=None, body=None):
        request_url = self._api_url + endpoint
        request_args = dict(params=query_params, json=body, headers=self._headers, **self._settings)
        async with self.http_session.request(method, request_url, **request_args) as response:
            if response.status_code in {http_codes.ok, http_codes.created}:
                try:
                    return response.json()
                # etc.

This way, all HTTP requests will use the same session (single TCP handshake).

You could also directly offer the enduser the opportunity to start the session on its own (for example when the server starts).

how do i add Query parameters?

hello i create a python script for booking flights
i am getting offers like this:
` offers_list =duffel.offer_requests.create().slices(slices).passengers([{"type": "adult"}]).return_offers().execute()
id like to add query parameters for
max connections
and sort by total amount
any idea how to add it to the offer requests?

https://duffel.com/docs/api/v1/offers/get-offers
query parameters

`

Use property getters and setters for validation

Describe the bug
To ensure that validation of attributes happens every time they're set, we should use @property getters/setters for attributes requiring validation.

To Reproduce
n/a

Expected behavior
Use @property getters/setters to validate instance attributes - e.g. here:

    def __init__(self, client, order_id):
        """Instantiate an order change request creation."""
        self._client = client
        self._order_id = order_id
        self._slices = []
        OrderChangeRequestCreate._validate_order_id(self._order_id)

    def _validate_order_id(order_id):
        """Set order ID"""
        if type(order_id) is not str:
            raise OrderChangeRequestCreate.InvalidOrderId(order_id)

can be changed to:

    def __init__(self, client, order_id):
        """Instantiate an order change request creation."""
        self.client = client
        self.order_id = order_id
        self.slices = []

    @property
    def order_id(self):
        return self._order_id

    @order_id.setter
    def order_id(self, value):
        if not isinstance(value, str):
            raise OrderChangeRequestCreate.InvalidOrderId(value)

        self._order_id = value

System (please complete the following information):
n/a

Additional context
Could also fix this issue at the same time!

Always raise a more specific exception than Exception

Describe the bug
In some places, Exception is raised - this isn't good practice as it makes it almost impossible for the users of the library to catch this specific exception without hiding bugs - see discussion here.

To Reproduce
n/a

Expected behavior
It’s much better to define a custom Exception class, or use one of the built-ins (e.g. RuntimeError).

Examples around here - in this case, InvalidResponseError might be a better exception type.

System (please complete the following information):
n/a

Additional context
n/a

ValueError on most offer requests api calls

Hi there :)

I'm getting a ValueError("time data '2023-05-13T03:59:00.000Z' does not match format '%Y-%m-%dT%H:%M:%SZ'") on most of my api calls with the python client.

Here is the code to reproduce:

from duffel_api import Duffel

duffel = Duffel(access_token="MY_TOKEN")

response = (duffel.offer_requests.create()
            .cabin_class("economy")
            .passengers([{"type": "adult"}])
            .max_connections(2)
            .return_offers()
            .slices([
                    {"origin": "PAR", "destination": "LON", "departure_date": "2023-06-14"},
                    {"origin": "LON", "destination": "PAR", "departure_date": "2023-06-16"},
                ]
            )
            .execute())

Expected behavior
No exception is raised

System

  • OS: MacOS
  • Library version: 0.5.0
  • Python Version: 3.11.2

Add type hints for models

Is your feature request related to a problem? Please describe.
Type hints make it significantly easier for clients to use the SDK.

Describe the solution you'd like
Type hints would be great to see for the models module especially, since this part of the codebase is external-facing.

This makes it significantly easier for developers to use the library because they will clearly know what constitutes expected values for class attributes and function params.

Additionally, many IDEs support on-hover definitions of classes which include the attributes' types when they're defined, again making developers' lives easier.

Describe alternatives you've considered
An interim solution would be to provide docstrings for every externally-used model class, containing the type hints, e.g.

def foo(a, n, *, some_opt=False):
    """
    A foo that bars.

    Arguments:
        a {str} - An 'a' string.
        n {int} - The number of times to print 'a'.

    Keyword Arguments:
        some_opt {bool} - Whether to do some option or not.
    """
    ...

This isn't verifiable via mypy so is a worse solution, but at least would give developers a better idea of the expected types.

Suggestion: use Pydantic for data parsing (and type validation)

Is your feature request related to a problem? Please describe.
There is no problem involved with this issue, its only a codebase suggestion.

Additional context
Maintaining data parsing and validation is kind of painful and not so fun, using a tool like Pydantic might save you some energy (for example you woudln't have to write complete from_json but only provide a way to parse specific parts of it when you have unusual rules).

First step
A first step could simply be to use Pydantic's @dataclass instead of the default one without using any of the data validation.

Pros/cons

Pros:

  • simpler parsing code -> no code in most cases (easier to maintain);
  • more complete (dynamic) type checking;
  • more standard way of doing type validation (which again make it easier to maintain);
  • can easily generate openAPI schema, which can help propagate types to frontend;
  • integrates nicely with your linter;
  • benefit from the great work of others, Pydantic is quite fast (V2 was just released and promise great performance improvements)

Cons:

  • you become dependant on Pydantic (but its quite widely used mainly thanks to FastAPI);
  • transition is not that easy (requires strong tests to make sure nothing breaks, but regression testing based on output types should suffice).

Airport example

As an example, take the Airport data class that contain a nested City data class to compare the Pydantic implementation with the current one.

Current implementation

Here is the code you have now (its fully working as is this is why I kept the get_and_transform function in here):

from typing import Optional
from dataclasses import dataclass

@dataclass
class City:
    id: str
    name: str
    iata_code: str
    iata_country_code: str

    @classmethod
    def from_json(cls, json: dict):
        return cls(
            id=json["id"],
            name=json["name"],
            iata_code=json["iata_code"],
            iata_country_code=json["iata_country_code"],
        )


@dataclass
class Airport:
    id: str
    name: str
    iata_code: Optional[str]
    icao_code: Optional[str]
    iata_country_code: str
    latitude: float
    longitude: float
    time_zone: str
    city: Optional[City]

    @classmethod
    def from_json(cls, json: dict):
        return cls(
            id=json["id"],
            name=json["name"],
            iata_code=json.get("iata_code"),
            icao_code=json.get("icao_code"),
            iata_country_code=json["iata_country_code"],
            latitude=json["latitude"],
            longitude=json["longitude"],
            time_zone=json["time_zone"],
            city=get_and_transform(json, "city", City.from_json),
        )
    

def get_and_transform(dict: dict, key: str, fn, default=None):
    try:
        value = dict[key]
        if value is None:
            return value
        else:
            return fn(value)
    except KeyError:
        return default

And here is how it is called:

>>> Airport.from_json(airport_json)
Airport(id='arp_swf_us', name='New York Stewart International Airport', iata_code='SWF', icao_code='KSWF', iata_country_code='US', latitude=41.501292, longitude=-74.102724, time_zone='America/New_York', city=City(id='cit_nyc_us', name='New York', iata_code='NYC', iata_country_code='US'))

Pydantic version

from pydantic import BaseModel

class PydanticCity(BaseModel):
    id: str
    name: str
    iata_code: str
    iata_country_code: str


class PydanticAirport(BaseModel):
    id: str
    name: str
    iata_code: Optional[str]
    icao_code: Optional[str]
    iata_country_code: str
    latitude: float
    longitude: float
    time_zone: str
    city: Optional[City]

And here is how it would called (using the BaseModel.model_validate method):

>>> PydanticAirport.model_validate(airport_json)
PydanticAirport(id='arp_swf_us', name='New York Stewart International Airport', iata_code='SWF', icao_code='KSWF', iata_country_code='US', latitude=41.501292, longitude=-74.102724, time_zone='America/New_York', city=City(id='cit_nyc_us', name='New York', iata_code='NYC', iata_country_code='US'))

Stats for the geeks

The performances of the two validations are as follows:

>>> %timeit Airport.from_json(airport_json)
861 ns ± 11.2 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

>>> %timeit PydanticAirport.model_validate(airport_json)
1.79 µs ± 16.5 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

Note that Pydantic performs a complete validation (each field is type checked) whereas your current code only parse the input data. The comparaison is not made to be fair, I just wanted to highlight that there isn't a huge performance difference between the two (Pydantic is basically twice as slow as you current implementation).

Type errors

>>> airport_json["iata_country_code"] = 12
>>> Airport.from_json(airport_json)
# no error
>>> PydanticAirport.model_validate(airport_json)
ValidationError: 1 validation error for PydanticAirport
iata_country_code
  Input should be a valid string [type=string_type, input_value=12, input_type=int]
    For further information visit https://errors.pydantic.dev/2.0.3/v/string_type

Pydantic dataclasses

from typing import Optional
from pydantic.dataclasses import dataclass


@dataclass
class PydanticCity:
    id: str
    name: str
    iata_code: str
    iata_country_code: str


@dataclass
class PydanticAirport:
    id: str
    name: str
    iata_code: Optional[str]
    icao_code: Optional[str]
    iata_country_code: str
    latitude: float
    longitude: float
    time_zone: str
    city: Optional[City]

From the Pydantic's dataclasses documentation:

Keep in mind that pydantic.dataclasses.dataclass is not a replacement for pydantic.BaseModel. pydantic.dataclasses.dataclass provides a similar functionality to dataclasses.dataclass with the addition of Pydantic validation. There are cases where subclassing pydantic.BaseModel is the better choice.

For more information and discussion see pydantic/pydantic#710.

Disclaimer

I'm not a maintainer of Pydantic, nor I have any sort of participation in it (I think I've never even raised an issue there). I just like it.

Class initialisation from JSON

Is your feature request related to a problem? Please describe.
Currently, classes in duffel_api/models take a json param in the __init__ method and loop through everything in the dict to set the attrs.

This isn’t optimal for type-checking, and also makes the user experience worse because it’s unclear what the attributes of a class are for users of the library (they’d have to go and check the API docs in this case, but it’d be nice if they could get access to this info in the SDK - especially with on-hover definitions in many IDEs).

Describe the solution you'd like
A way around this is to reserve __init__ to explicitly list out all attributes of the class, and take all attributes as explicit keyword args. This can be used in conjunction with a .from_json(json) classmethod which unpacks the dict and passes it to the class initialiser.

  • At a glance, users can see the attributes (and types) for this class.
    • This is especially useful when your IDE supports docs in on-hover defs!
    • #81 could also be addressed well at the same time.
  • All expected fields will raise a KeyError if not present in the JSON (since this would indicate our API wasn't returning some expected data).
  • All non-required fields will use a default.

As a toy example, this:

class OfferRequest:
    def __init__(self, json):
        for key in json:
            value = json[key]

            if isinstance(value, str):
                value = maybe_parse_date_entries(key, json[key])
                setattr(self, key, value)
                continue

            if isinstance(value, list):
                if key == "offers":
                    value = [Offer(v) for v in value]
                elif key == "passengers":
                    value = [Passenger(v) for v in value]
                elif key == "slices":
                    value = [Slice(v) for v in value]

            setattr(self, key, value)

could be changed to this:

class OfferRequest:
    def __init__(
        self,
        passengers: List[Passenger],
        slices: List[Slices],
        cabin_class: str,
        offers: List[Offer],
        live_mode: bool,
        offer_request_id: str,
        created_at: datetime.datetime,
):
    self.passengers: List[Passenger] = passengers
    self.slices: List[Slice] = slices
    self.cabin_class: str = cabin_class
    self.live_mode: bool = live_mode
    self.offer_request_id: str = offer_request_id
    self.created_at: datetime.datetime = created_at
    self.offers: List[Offer] = offers

    @classmethod
    def from_json(cls, json: dict):
        """Construct a class instance from a JSON response."""
        return cls(
            passengers=[Passenger(p) for p in json["passengers"]],
            slices=[Slice(s) for s in json["slices"]],
            live_mode=json["live_mode"],
            offer_request_id=json["offer_request_id"],
            cabin_class=json["cabin_class"],
            created_at=maybe_parse_date_entries("created_at", json["created_at"]),
            offers=[Offer[o] for o in json.get("offers", [])],
        )

Describe alternatives you've considered
The most obvious alternative is the way things are currently done (loop through a JSON dict, setting attributes as you go) - but it's way less clear to users of the SDK what attributes a given class has.

Additional context
n/a

Incorrect filtering

Hello. I am trying to use the python library in one of my apps. However, it throws an error if I provide any parameter other than departure_date, destination and origin.

In the documentation, and the example in documentation, its clearly mentioned that arrival_time and departure_time can also be param in slice.

FWIW, I commented the error, and it worked - which means backend is alright, but the python code is not working.

Official Documentation:


from duffel_api import Duffel

duffel = Duffel(access_token=YOUR_ACCESS_TOKEN)

duffel.offer_requests.create().cabin_class("economy")

.passengers([
   {
      "family_name": "Earhart",
      "given_name": "Amelia",
      "loyalty_programme_accounts": [
         {
            "account_number": "12901014",
            "airline_iata_code": "BA"
         }
      ],
      "type": "adult"
   },
   {
      "age": 14
   },
   {
      "fare_type": "student"
   },
   {
      "age": 5,
      "fare_type": "contract_bulk_child"
   }
])
.slices([
   {
      "origin": "LHR",
      "destination": "JFK",
      "departure_time": {
         "to": "17:00",
         "from": "09:45"
      },
      "departure_date": "2020-04-24",
      "arrival_time": {
         "to": "17:00",
         "from": "09:45"
      }
   }
])
.execute()

Line with Issue:

if set(travel_slice.keys()) != set(


 @staticmethod
    def _validate_slices(slices):
        """Validate number of slices and the data provided for each if any were given"""
        if len(slices) == 0:
            raise OfferRequestCreate.InvalidNumberOfSlices(slices)
        for travel_slice in slices:
            if set(travel_slice.keys()) != set(
                ["departure_date", "destination", "origin"]
            ):
                raise OfferRequestCreate.InvalidSlice(travel_slice)

Use isinstance rather than type when type-checking

Describe the bug
Some places use type() instead of isinstance() when checking types (e.g. here). Using isinstance() is preferred because it's faster and also considers inheritance (and you’d probably want to still consider subclasses of dict as true in that example above - e.g. if we wanted to use an OrderedDict).

To Reproduce
n/a

Expected behavior
Check types with isinstance() instead.

System (please complete the following information):
n/a

Additional context
n/a

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.