GithubHelp home page GithubHelp logo

ahivert / tgtg-python Goto Github PK

View Code? Open in Web Editor NEW
361.0 22.0 69.0 427 KB

Unofficial client for TooGoodToGo API

License: GNU General Public License v3.0

Makefile 0.31% Python 99.69%
api-client toogoodtogo

tgtg-python's Introduction

Actions Status codecov PyPI version

tgtg-python

Python client that help you to talk with TooGoodToGo API.

Python version: 3.8+

Handle:

  • create an account (/api/auth/vX/signUpByEmail)
  • login (/api/auth/vX/authByEmail)
  • refresh token (/api/auth/vX/token/refresh)
  • list stores (/api/item/vX)
  • get a store (/api/item/vX/:id)
  • get favorites (/api/discover/vX/bucket)
  • set favorite (/api/user/favorite/vX/:id/update)
  • create an order (/api/order/vX/create/:id)
  • abort an order (/api/order/vX/:id/abort)
  • get the status of an order (/api/order/vX/:id/status)
  • get active orders (/api/order/vX/active)
  • get inactive orders (/api/order/vX/inactive)

Install

pip install tgtg

Use it

Retrieve tokens

Build the client with your email

from tgtg import TgtgClient

client = TgtgClient(email="<your_email>")
credentials = client.get_credentials()

You should receive an email from tgtg. The client will wait until you validate the login by clicking the link inside the email.

Once you clicked the link, you will get credentials and be able to use them

print(credentials)
{
    'access_token': '<your_access_token>',
    'refresh_token': '<your_refresh_token>',
    'user_id': '<your_user_id>',
    'cookie': '<cookie>',
}

Build the client from tokens

from tgtg import TgtgClient

client = TgtgClient(access_token="<access_token>", refresh_token="<refresh_token>", user_id="<user_id>", cookie="<cookie>")

Get items

# You can then get some items, by default it will *only* get your favorites
items = client.get_items()
print(items)

# To get items (not only your favorites) you need to provide location informations
items = client.get_items(
    favorites_only=False,
    latitude=48.126,
    longitude=-1.723,
    radius=10,
)
print(items)
Example response
[
    {
        "item": {
            "item_id": "64346",
            "item_price": {"code": "EUR", "minor_units": 499, "decimals": 2},
            "sales_taxes": [],
            "tax_amount": {"code": "EUR", "minor_units": 0, "decimals": 2},
            "price_excluding_taxes": {"code": "EUR", "minor_units": 499, "decimals": 2},
            "price_including_taxes": {"code": "EUR", "minor_units": 499, "decimals": 2},
            "value_excluding_taxes": {
                "code": "EUR",
                "minor_units": 1500,
                "decimals": 2,
            },
            "value_including_taxes": {
                "code": "EUR",
                "minor_units": 1500,
                "decimals": 2,
            },
            "taxation_policy": "PRICE_INCLUDES_TAXES",
            "show_sales_taxes": False,
            "value": {"code": "EUR", "minor_units": 1500, "decimals": 2},
            "cover_picture": {
                "picture_id": "110628",
                "current_url": "https://images.tgtg.ninja/item/cover/2b69cbdd-43d3-4ade-bd51-50e338260859.jpg",
            },
            "logo_picture": {
                "picture_id": "110618",
                "current_url": "https://images.tgtg.ninja/store/fb893813-a775-4dec-ac7b-d4a7dd326fa8.png",
            },
            "name": "",
            "description": "Salva comida en Ecofamily Bufé y tu pack podrá contener: comidas caseras.",
            "can_user_supply_packaging": False,
            "packaging_option": "MUST_BRING_BAG",
            "collection_info": "",
            "diet_categories": [],
            "item_category": "MEAL",
            "badges": [
                {
                    "badge_type": "SERVICE_RATING_SCORE",
                    "rating_group": "LIKED",
                    "percentage": 93,
                    "user_count": 178,
                    "month_count": 5,
                }
            ],
            "favorite_count": 0,
            "buffet": False,
        },
        "store": {
            "store_id": "59949s",
            "store_name": "Ecofamily Bufé - Centro",
            "branch": "",
            "description": "",
            "tax_identifier": "",
            "website": "",
            "store_location": {
                "address": {
                    "country": {"iso_code": "ES", "name": "Spain"},
                    "address_line": "Av. de los Piconeros, S/N, 14001 Córdoba, España",
                    "city": "",
                    "postal_code": "",
                },
                "location": {"longitude": -4.776045, "latitude": 37.894249},
            },
            "logo_picture": {
                "picture_id": "110618",
                "current_url": "https://images.tgtg.ninja/store/fb893813-a775-4dec-ac7b-d4a7dd326fa8.png",
            },
            "store_time_zone": "Europe/Madrid",
            "hidden": False,
            "favorite_count": 0,
            "we_care": False,
        },
        "display_name": "Ecofamily Bufé - Centro",
        "pickup_location": {
            "address": {
                "country": {"iso_code": "ES", "name": "Spain"},
                "address_line": "Av. de los Piconeros, S/N, 14001 Córdoba, España",
                "city": "",
                "postal_code": "",
            },
            "location": {"longitude": -4.776045, "latitude": 37.894249},
        },
        "items_available": 0,
        "distance": 4241.995584076078,
        "favorite": True,
        "in_sales_window": False,
        "new_item": False,
    },
]

Get an item

(Using item_id from get_items response)

item = client.get_item(item_id=614318)
print(item)
Example response
{
    "item": {
        "item_id": "614318",
        "sales_taxes": [{"tax_description": "TVA", "tax_percentage": 5.5}],
        "tax_amount": {"code": "EUR", "minor_units": 13, "decimals": 2},
        "price_excluding_taxes": {"code": "EUR", "minor_units": 236, "decimals": 2},
        "price_including_taxes": {"code": "EUR", "minor_units": 249, "decimals": 2},
        "value_excluding_taxes": {"code": "EUR", "minor_units": 0, "decimals": 2},
        "value_including_taxes": {"code": "EUR", "minor_units": 0, "decimals": 2},
        "taxation_policy": "PRICE_INCLUDES_TAXES",
        "show_sales_taxes": False,
        "cover_picture": {
            "picture_id": "620171",
            "current_url": "https://images.tgtg.ninja/item/cover/ac80c1b3-1386-46a8-ba80-c97b3a6e7e18.png",
            "is_automatically_created": False,
        },
        "logo_picture": {
            "picture_id": "622046",
            "current_url": "https://images.tgtg.ninja/store/6280890a-729c-400b-89d8-8b6d5b6cc17b.png",
            "is_automatically_created": False,
        },
        "name": "Panier petit déjeuner",
        "description": "Sauvez un panier-surprise réalisé à partir des délicieux articles d'un buffet petit déjeuner.",
        "food_handling_instructions": "",
        "can_user_supply_packaging": False,
        "packaging_option": "BAG_ALLOWED",
        "collection_info": "",
        "diet_categories": [],
        "item_category": "BAKED_GOODS",
        "buffet": True,
        "badges": [
            {
                "badge_type": "SERVICE_RATING_SCORE",
                "rating_group": "LOVED",
                "percentage": 96,
                "user_count": 131,
                "month_count": 6,
            },
            {
                "badge_type": "OVERALL_RATING_TRUST_SCORE",
                "rating_group": "LOVED",
                "percentage": 90,
                "user_count": 131,
                "month_count": 6,
            },
        ],
        "positive_rating_reasons": [
            "POSITIVE_FEEDBACK_FRIENDLY_STAFF",
            "POSITIVE_FEEDBACK_GREAT_QUANTITY",
            "POSITIVE_FEEDBACK_QUICK_COLLECTION",
            "POSITIVE_FEEDBACK_DELICIOUS_FOOD",
            "POSITIVE_FEEDBACK_GREAT_VALUE",
            "POSITIVE_FEEDBACK_GREAT_VARIETY",
        ],
        "average_overall_rating": {
            "average_overall_rating": 4.520325203252033,
            "rating_count": 123,
            "month_count": 6,
        },
        "allergens_info": {"shown_on_checkout": False},
        "favorite_count": 0,
    },
    "store": {
        "store_id": "624740",
        "store_name": "Hôtel Les Matins de Paris & Spa",
        "branch": "",
        "description": "Vous y êtes. Où ? À South Pigalle (Sopi pour les adeptes), au coeur d’une décontraction trendy.\nVous en êtes : de ceux qui ont déniché un lieu joliment habité, là où se fredonne depuis tant de décennies des airs vivement enjoués. Parce que, pour la petite histoire, notre adresse fut dans les années 50' 60' le repaire des plus arty.\nPile ici, le premier restaurant américain parisien créé par le fantaisiste Leroy Haynes attirait un heureux tohu-bohu, une kyrielle de musiciens, de Ray Charles à Marianne Faithfull…\nAujourd'hui, à vous d'improviser ici un rendez-vous amical, à vous de composer là avec la paresse la plus joyeuse. Se mettre au voluptueux diapason du spa, suivre le rythme des conseils spontanés d’une équipe concernée sont aussi des moments pour vous écouter.\nEntendez-vous la petite musique du lieu ? L'âme des Matins de Paris donne assurément le bon tempo pour prendre ses quartiers, les plus inspirés. ",
        "tax_identifier": "FR43552132029",
        "website": "https://www.lesmatinsdeparis.com/",
        "store_location": {
            "address": {
                "country": {"iso_code": "FR", "name": "France"},
                "address_line": "3 Rue Clauzel, 75009 Paris, France",
                "city": "",
                "postal_code": "",
            },
            "location": {"longitude": 2.3393925, "latitude": 48.8788434},
        },
        "logo_picture": {
            "picture_id": "622046",
            "current_url": "https://images.tgtg.ninja/store/6280890a-729c-400b-89d8-8b6d5b6cc17b.png",
            "is_automatically_created": False,
        },
        "store_time_zone": "Europe/Paris",
        "hidden": False,
        "favorite_count": 0,
        "items": [
            {
                "item": {
                    "item_id": "614318",
                    "sales_taxes": [{"tax_description": "TVA", "tax_percentage": 5.5}],
                    "tax_amount": {"code": "EUR", "minor_units": 13, "decimals": 2},
                    "price_excluding_taxes": {
                        "code": "EUR",
                        "minor_units": 236,
                        "decimals": 2,
                    },
                    "price_including_taxes": {
                        "code": "EUR",
                        "minor_units": 249,
                        "decimals": 2,
                    },
                    "value_excluding_taxes": {
                        "code": "EUR",
                        "minor_units": 0,
                        "decimals": 2,
                    },
                    "value_including_taxes": {
                        "code": "EUR",
                        "minor_units": 0,
                        "decimals": 2,
                    },
                    "taxation_policy": "PRICE_INCLUDES_TAXES",
                    "show_sales_taxes": False,
                    "cover_picture": {
                        "picture_id": "620171",
                        "current_url": "https://images.tgtg.ninja/item/cover/ac80c1b3-1386-46a8-ba80-c97b3a6e7e18.png",
                        "is_automatically_created": False,
                    },
                    "logo_picture": {
                        "picture_id": "622046",
                        "current_url": "https://images.tgtg.ninja/store/6280890a-729c-400b-89d8-8b6d5b6cc17b.png",
                        "is_automatically_created": False,
                    },
                    "name": "Panier petit déjeuner",
                    "description": "Sauvez un panier-surprise réalisé à partir des délicieux articles d'un buffet petit déjeuner.",
                    "food_handling_instructions": "",
                    "can_user_supply_packaging": False,
                    "packaging_option": "BAG_ALLOWED",
                    "collection_info": "",
                    "diet_categories": [],
                    "item_category": "BAKED_GOODS",
                    "buffet": True,
                    "badges": [
                        {
                            "badge_type": "SERVICE_RATING_SCORE",
                            "rating_group": "LOVED",
                            "percentage": 96,
                            "user_count": 131,
                            "month_count": 6,
                        },
                        {
                            "badge_type": "OVERALL_RATING_TRUST_SCORE",
                            "rating_group": "LOVED",
                            "percentage": 90,
                            "user_count": 131,
                            "month_count": 6,
                        },
                    ],
                    "positive_rating_reasons": [
                        "POSITIVE_FEEDBACK_FRIENDLY_STAFF",
                        "POSITIVE_FEEDBACK_GREAT_QUANTITY",
                        "POSITIVE_FEEDBACK_QUICK_COLLECTION",
                        "POSITIVE_FEEDBACK_DELICIOUS_FOOD",
                        "POSITIVE_FEEDBACK_GREAT_VALUE",
                        "POSITIVE_FEEDBACK_GREAT_VARIETY",
                    ],
                    "average_overall_rating": {
                        "average_overall_rating": 4.520325203252033,
                        "rating_count": 123,
                        "month_count": 6,
                    },
                    "favorite_count": 0,
                },
                "display_name": "Hôtel Les Matins de Paris & Spa (Panier petit déjeuner)",
                "pickup_interval": {
                    "start": "2022-11-04T11:00:00Z",
                    "end": "2022-11-04T15:00:00Z",
                },
                "pickup_location": {
                    "address": {
                        "country": {"iso_code": "FR", "name": "France"},
                        "address_line": "3 Rue Clauzel, 75009 Paris, France",
                        "city": "",
                        "postal_code": "",
                    },
                    "location": {"longitude": 2.3393925, "latitude": 48.8788434},
                },
                "purchase_end": "2022-11-04T15:00:00Z",
                "items_available": 0,
                "sold_out_at": "2022-11-03T17:11:32Z",
                "distance": 0.0,
                "favorite": True,
                "in_sales_window": True,
                "new_item": False,
            }
        ],
        "milestones": [
            {"type": "MEALS_SAVED", "value": "250"},
            {"type": "MONTHS_ON_PLATFORM", "value": "6"},
        ],
        "we_care": False,
        "distance": 0.0,
        "cover_picture": {
            "picture_id": "620171",
            "current_url": "https://images.tgtg.ninja/item/cover/ac80c1b3-1386-46a8-ba80-c97b3a6e7e18.png",
            "is_automatically_created": False,
        },
        "is_manufacturer": False,
    },
    "display_name": "Hôtel Les Matins de Paris & Spa (Panier petit déjeuner)",
    "pickup_interval": {"start": "2022-11-04T11:00:00Z", "end": "2022-11-04T15:00:00Z"},
    "pickup_location": {
        "address": {
            "country": {"iso_code": "FR", "name": "France"},
            "address_line": "3 Rue Clauzel, 75009 Paris, France",
            "city": "",
            "postal_code": "",
        },
        "location": {"longitude": 2.3393925, "latitude": 48.8788434},
    },
    "purchase_end": "2022-11-04T15:00:00Z",
    "items_available": 0,
    "sold_out_at": "2022-11-03T17:11:32Z",
    "distance": 0.0,
    "favorite": True,
    "in_sales_window": True,
    "new_item": False,
    "sharing_url": "https://share.toogoodtogo.com/download?locale=fr-FR",
    "next_sales_window_purchase_start": "2022-11-04T15:17:00Z",
}

Create an order

order = client.create_order(item_id, number_of_items_to_order)
print(order)
Example response
{
  "id": "<order_id>",
  "item_id": "<item_id_that_was_ordered>",
  "user_id": "<your_user_id>",
  "state": "RESERVED",
  "order_line": {
    "quantity": 1,
    "item_price_including_taxes": {
      "code": "EUR",
      "minor_units": 600,
      "decimals": 2
    },
    "item_price_excluding_taxes": {
      "code": "EUR",
      "minor_units": 550,
      "decimals": 2
    },
    "total_price_including_taxes": {
      "code": "EUR",
      "minor_units": 600,
      "decimals": 2
    },
    "total_price_excluding_taxes": {
      "code": "EUR",
      "minor_units": 550,
      "decimals": 2
    }
  },
  "reserved_at": "2023-01-01T10:30:32.331280392",
  "order_type": "MAGICBAG"
}

Please note that payment of an order is currently not implemented. In other words: you can create an order via this client, but you can not pay for it.

Get the status of an order

order_status = client.get_order_status(order_id)
print(order_status)
Example response
{
  "id": "<order_id>",
  "item_id": "<item_id_that_was_ordered>",
  "user_id": "<your_user_id>",
  "state": "RESERVED"
}

Abort an order

client.abort_order(order_id)

When successful, this call will not return a value.

The app uses this call when the user aborts an order before paying for it. When the order has been payed, the app uses a different call.

Get active orders

active = client.get_active()
print(active)

Get inactive orders

client.get_inactive(page=0, page_size=20)

# returned object has `has_more` property if more results are available

To e.g. sum up all orders you have ever made:

    orders = []
    page = 0
    while inactive := client.get_inactive(page=page, page_size=200):
        orders += inactive["orders"]
        if not inactive["has_more"]:
            break

    redeemed_orders = [x for x in orders if x["state"] == "REDEEMED"]
    redeemed_items = sum([x["quantity"] for x in redeemed_orders])

    # if you bought in multiple currencies this will need improvements
    money_spend = sum(
        [
            x["price_including_taxes"]["minor_units"]
            / (10 ** x["price_including_taxes"]["decimals"])
            for x in redeemed_orders
        ]
    )

    print(f"Total numbers of orders: {len(orders)}")
    print(f"Total numbers of picked up orders: {len(redeemed_orders)}")
    print(f"Total numbers of items picked up: {redeemed_items}")
    print(
        f"Total money spend: ~{money_spend:.2f}{redeemed_orders[0]['price_including_taxes']['code']}"
    )

Get favorites

This will list all the currently set favorite stores.

favorites = client.get_favorites()
print(favorites)

The behavior of get_favorites is more or less the same as get_items(), but better mimics the official application.

Set favorite

(Using item_id from get_items response)

# add favorite
client.set_favorite(item_id=64346, is_favorite=True)

# remove favorite
client.set_favorite(item_id=64346, is_favorite=False)

Create an account

from tgtg import TgtgClient

client = TgtgClient()
client.signup_by_email(email="<your_email>")

# client is now ready to be used

Developers

This project uses poetry so you will need to install poetry locally to use following commands.

pip install poetry --user
poetry install

This project uses pre-commit to format/check all the code before each commit automatically.

pip install pre-commit --user
pre-commit install

Run this command to run all tests:

make test

tgtg-python's People

Contributors

ahivert avatar brandonbondig avatar chouffy avatar dependabot-preview[bot] avatar dependabot[bot] avatar der-henning avatar desastrenatural avatar dielee avatar dl6er avatar floriegl avatar goegol avatar inetant avatar jurrie avatar justarandomguyintheinternet avatar lucas-vdr-horst avatar matkoniecz avatar maxwinterstein avatar nitrikx avatar pre-commit-ci[bot] avatar tjorim avatar trietsch avatar xathon 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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

tgtg-python's Issues

Figuring out the API

Hi,

I've been spending my day on trying to reverse engineer the TooGoodToGo APK.

I found some interesting URLs in the APK:

package com.app.tgtg.p137f.p138c;

/* renamed from: com.app.tgtg.f.c.a */
public interface ApiService {
    @POST("api/auth/v1/token/refresh")
    /* renamed from: a */
    Maybe<C6979q<RefreshTokenResult>> mo10819a(@Body RefreshToken refreshToken);

    @POST("api/hiddenstore/v1/remove")
    /* renamed from: a */
    Maybe<RemoveHiddenStoreResponse> mo10820a(@Body RemoveHiddenStoreRequest removeHiddenStoreRequest);

    @POST("api/hiddenstore/v1/unlock")
    /* renamed from: a */
    Maybe<UnlockHiddenStoreResponse> mo10821a(@Body UnlockHiddenStoreRequest unlockHiddenStoreRequest);

    @POST("api/item/v4/")
    /* renamed from: a */
    Maybe<ListItemResponse> mo10822a(@Body ListItemRequest listItemRequest);

    @POST("api/location/v1/lookup")
    /* renamed from: a */
    Maybe<ReverseLookupResponse> mo10823a(@Body GeoLocation geoLocation);

    @POST("api/location/v1/search")
    /* renamed from: a */
    Maybe<SearchLocationResponse> mo10824a(@Body LocationRequest locationRequest);

    @POST("api/payment/v1/recurring/delete")
    /* renamed from: a */
    Maybe<ResponseBody> mo10825a(@Body DeleteRecurringPaymentOption deleteRecurringPaymentOption);

    @POST("api/payment/v1/option/list")
    /* renamed from: a */
    Maybe<ListPaymentOptionsResponse> mo10826a(@Body ItemBasketRequest itemBasketRequest);

    @POST("api/voucher/v2/add")
    /* renamed from: a */
    Maybe<AddVoucherResponse> mo10827a(@Body AddVoucherRequest addVoucherRequest);

    @POST("api/basket/v2/{basketId}/cancel")
    /* renamed from: a */
    Maybe<ResponseBody> mo10828a(@Path(encoded = true, value = "basketId") String str);

    @POST("api/item/v4/{itemId}")
    /* renamed from: a */
    Maybe<Item> mo10829a(@Path(encoded = true, value = "itemId") String str, @Body ItemRequest itemRequest);

    @POST("api/item/v4/{itemId}/setFavorite")
    /* renamed from: a */
    Maybe<ResponseBody> mo10830a(@Path(encoded = true, value = "itemId") String str, @Body SetFavoriteRequest setFavoriteRequest);

    @POST("api/basket/v2/{basketId}/checkout")
    /* renamed from: a */
    Maybe<CheckoutResult> mo10831a(@Path(encoded = true, value = "basketId") String str, @Body BasketCheckout basketCheckout);

    @POST("api/basket/v2/{basketId}/complete")
    /* renamed from: a */
    Maybe<BasketCompleteResponse> mo10832a(@Path(encoded = true, value = "basketId") String str, @Body BasketComplete basketComplete);

    @POST("api/item/v4/{itemId}/basket")
    /* renamed from: a */
    Maybe<Basket> mo10833a(@Path(encoded = true, value = "itemId") String str, @Body ItemBasketRequest itemBasketRequest);

    @POST("api/basket/v2/{basketId}/recurring/save")
    /* renamed from: a */
    Maybe<C6979q<ResponseBody>> mo10834a(@Path(encoded = true, value = "basketId") String str, @Body SaveCardRequest saveCardRequest);

    @POST("api/auth/v1/token/refresh")
    /* renamed from: a */
    Object mo10835a(@Body RefreshToken refreshToken, Continuation<? super C6979q<RefreshTokenResult>> cVar);

    @POST("api/user/v1/update")
    /* renamed from: a */
    Object mo10836a(@Body UserData userData, Continuation<? super UserData> cVar);

    @POST("api/item/v4/discover")
    /* renamed from: a */
    Object mo10837a(@Body DiscoverItemsRequest discoverItemsRequest, Continuation<? super DiscoverItemsResponse> cVar);

    @POST("api/tracking/v1/heartbeat")
    /* renamed from: a */
    Object mo10838a(@Body HeartbeatRequest heartbeatRequest, Continuation<? super ResponseBody> cVar);

    @POST("api/tracking/v1/impressions")
    /* renamed from: a */
    Object mo10839a(@Body ItemImpressionRequest itemImpressionRequest, Continuation<? super ResponseBody> cVar);

    @POST("api/item/v4/")
    /* renamed from: a */
    Object mo10840a(@Body ListItemRequest listItemRequest, Continuation<? super ListItemResponse> cVar);

    @POST("api/order/v1/active")
    /* renamed from: a */
    Object mo10841a(@Body ListOrdersRequest listOrdersRequest, Continuation<? super OrderListResult> cVar);

    @POST("api/support/v1/business/")
    /* renamed from: a */
    Object mo10842a(@Body BusinessSupportRequest businessSupportRequest, Continuation<? super ResponseBody> cVar);

    @POST("api/support/v1/consumer/")
    /* renamed from: a */
    Object mo10843a(@Body ConsumerSupportRequest consumerSupportRequest, Continuation<? super ConsumerSupportResponse> cVar);

    @POST("api/support/v1/consumer/refund/choice")
    /* renamed from: a */
    Object mo10844a(@Body ConsumerRefundChoiceRequest consumerRefundChoiceRequest, Continuation<? super ResponseBody> cVar);

    @POST("api/user/v1/changePassword")
    /* renamed from: a */
    Object mo10845a(@Body ChangePasswordRequest changePasswordRequest, Continuation<? super ResponseBody> cVar);

    @POST("api/signup/v1/userExists")
    /* renamed from: a */
    Object mo10846a(@Body CheckUserExistsRequest checkUserExistsRequest, Continuation<? super C6979q<EmailCheckResult>> cVar);

    @POST("api/auth/v1/loginByEmail")
    /* renamed from: a */
    Object mo10847a(@Body LoginByEmailRequest loginByEmailRequest, Continuation<? super LoginResponse> cVar);

    @POST("api/auth/v1/loginByThirdParty")
    /* renamed from: a */
    Object mo10848a(@Body LoginByThirdPartyRequest loginByThirdPartyRequest, Continuation<? super LoginResponse> cVar);

    @POST("api/user/v1/resetPassword")
    /* renamed from: a */
    Object mo10849a(@Body ResetPasswordRequest resetPasswordRequest, Continuation<? super ResponseBody> cVar);

    @POST("api/auth/v1/signUpByEmail")
    /* renamed from: a */
    Object mo10850a(@Body SignUpByEmailRequest signUpByEmailRequest, Continuation<? super LoginResponse> cVar);

    @POST("api/auth/v1/signUpByThirdParty")
    /* renamed from: a */
    Object mo10851a(@Body SignUpByThirdPartyRequest signUpByThirdPartyRequest, Continuation<? super LoginResponse> cVar);

    @POST("api/voucher/v2/")
    /* renamed from: a */
    Object mo10852a(@Body VoucherListRequest voucherListRequest, Continuation<? super VoucherList> cVar);

    @POST("api/item/v4/{itemId}")
    /* renamed from: a */
    Object mo10853a(@Path(encoded = true, value = "itemId") String str, @Body ItemRequest itemRequest, Continuation<? super Item> cVar);

    @POST("api/item/v4/{itemId}/setFavorite")
    /* renamed from: a */
    Object mo10854a(@Path(encoded = true, value = "itemId") String str, @Body SetFavoriteRequest setFavoriteRequest, Continuation<? super ResponseBody> cVar);

    @POST("api/order/v1/{receiptId}/cancel")
    /* renamed from: a */
    Object mo10855a(@Path(encoded = true, value = "receiptId") String str, @Body CancelOrderRequest cancelOrderRequest, Continuation<? super ResponseBody> cVar);

    @POST("api/order/v1/{receiptId}/rate")
    /* renamed from: a */
    Object mo10856a(@Path(encoded = true, value = "receiptId") String str, @Body OrderRating orderRating, Continuation<? super C6979q<ResponseBody>> cVar);

    @POST("/order/v1/{receiptId}/sendOrderConfirmedEmail")
    /* renamed from: a */
    Object mo10857a(@Path(encoded = true, value = "receiptId") String str, @Body OrderConfirmedEmailRequest orderConfirmedEmailRequest, Continuation<? super ResponseBody> cVar);

    @POST("api/item/v4/{itemId}/basket")
    /* renamed from: a */
    Object mo10858a(@Path(encoded = true, value = "itemId") String str, @Body ItemBasketRequest itemBasketRequest, Continuation<? super Basket> cVar);

    @POST("api/gdpr/v1/{userId}/deleteUser")
    /* renamed from: a */
    Object mo10859a(@Path(encoded = true, value = "userId") String str, @Body DeleteUserRequest deleteUserRequest, Continuation<? super ResponseBody> cVar);

    @POST("api/gdpr/v1/{userId}/exportUserData")
    /* renamed from: a */
    Object mo10860a(@Path(encoded = true, value = "userId") String str, @Body ExportUserRequest exportUserRequest, Continuation<? super ResponseBody> cVar);

    @POST("api/store/v3/{storeId}")
    /* renamed from: a */
    Object mo10861a(@Path(encoded = true, value = "storeId") String str, @Body StoreRequest storeRequest, Continuation<? super StoreInformation> cVar);

    @POST("api/voucher/v2/{voucherId}")
    /* renamed from: a */
    Object mo10862a(@Path(encoded = true, value = "voucherId") String str, @Body VoucherDetailRequest voucherDetailRequest, Continuation<? super VoucherDetails> cVar);

    @POST("api/voucher/v2/{voucherId}/storeFilterList")
    /* renamed from: a */
    Object mo10863a(@Path(encoded = true, value = "voucherId") String str, @Body VoucherFilterRequest voucherFilterRequest, Continuation<? super VoucherFilterResponse> cVar);

    @POST("api/order/v1/{receiptId}")
    /* renamed from: a */
    Object mo10864a(@Path(encoded = true, value = "receiptId") String str, Continuation<? super Order> cVar);

    @POST("api/support/v1/uploading/files")
    @Multipart
    /* renamed from: a */
    Object mo10865a(@Part List<MultipartBody.C6476c> list, Continuation<? super SupportPictureUploadResponse> cVar);

    @POST("api/app/v1/user_settings")
    /* renamed from: a */
    Object mo10866a(Continuation<? super UserSettings> cVar);

    @POST("api/order/v1/inactive")
    /* renamed from: b */
    Object mo10867b(@Body ListOrdersRequest listOrdersRequest, Continuation<? super OrderListResult> cVar);

    @POST("api/user/v1/confirmEmail/{token}")
    /* renamed from: b */
    Object mo10868b(@Path(encoded = true, value = "token") String str, Continuation<? super C6979q<ResponseBody>> cVar);

    @POST("api/map/v1/listAllBusinessMap")
    /* renamed from: b */
    Object mo10869b(Continuation<? super StoreListResult> cVar);

    @POST("api/basket/v2/{basketId}/cancel")
    /* renamed from: c */
    Object mo10870c(@Path(encoded = true, value = "basketId") String str, Continuation<? super ResponseBody> cVar);

    @POST("api/app/v1/app_settings")
    /* renamed from: c */
    Object mo10871c(Continuation<? super AppSettings> cVar);

    @POST("api/order/v1/{receiptId}/redeem")
    /* renamed from: d */
    Object mo10872d(@Path(encoded = true, value = "receiptId") String str, Continuation<? super ResponseBody> cVar);

    @POST("api/user/v1/")
    /* renamed from: d */
    Object mo10873d(Continuation<? super UserData> cVar);

    @POST("api/map/v1/listAllBusinessLocationPicker")
    /* renamed from: e */
    Object mo10874e(Continuation<? super StoreLocationListResult> cVar);

    @POST("api/auth/v1/logout")
    /* renamed from: f */
    Object mo10875f(Continuation<? super Unit> cVar);

    @POST("api/app/v1/user_settings")
    /* renamed from: g */
    Object mo10876g(Continuation<? super UserSettings> cVar);
}

Finding those URLs is a great start

I think that by using Charles Proxy as a MITM we could easily see what the app is sending to the API. I will do that this week.

No results anymore after some time of continuous usage (reopened)

I was not able to reopen ticket #116

This problem still persists.
Items in the response of the api are not the same as in the app after some time. Around one item a day is returned by the api.

A new login via mail confirmation, without refresh token fixes this. But only for some time.

Howto do a order and automatic payment

I love a Pythonscript to order tgtg boxes, but does anyone have a working pythonscript so you can order and automatically pay for exampe through iDeal?

Unknown API point to get Scheduled Bag Order Times

For a few Months now Toogoodtogo has a new feature where it shows scheduled Order times. I.e. a store which gives out a bag everyday will show when the bag will go live to order.

See:
grafik

But the JSON from /api/item/ doesnt contain this data point.
Which means there probably is a separate API to request this data from.
I'm sadly not experienced in reverse Engineering API requests, else I would have tried to figure it out myself.

Would it be possible for someone more experienced to give it a try?
Thanks in advance and thank you for this great tool!

401 Unauthorized

I coded a monitor that checks every 10 seconds if a store in my area restocks.
After some time I run the script, i get a 401, unauthorized, so i'm guessing the token / auth expires.
Is there a way to renew it, without checking if i get the error code and inizializing the client every time?
Thanks.

TgtgAPIError 401

Dear all,

Since yesterday I have been receiving an error 401 when I call the client.get_items() function.
raise TgtgAPIError(response.status_code, response.content) tgtg.exceptions.TgtgAPIError: (401, b'{"timestamp":1635387434847,"status":401,"error":"Unauthorized","path":"/api/item/v7/"}')
What can I do to solve it? Is it related to the API? Any help is very much appreciated!

refresh token

Would it be possible to use the refresh token to get a new access token? With the current setup I have to manually go to my email to confirm the login everytime, but is it possible to only do this one time, and then let the refresh token get a new access token?

Does the API still work? TgtgLoginError

Dear ahivert,

I am getting a TgtgLoginError when I try a script that used to work some months ago. I guess that TGTG decided to take action against those unofficial libraries and services. Fx. TGTG-Notifier is not working anymore. The following is the exact error I get. Is there any way to make it work? Am I doing something wrong? I look forward to receiving your reply!

TgtgLoginError: (403, b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">\n<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">\n<TITLE>ERROR: The request could not be satisfied</TITLE>\n</HEAD><BODY>\n<H1>403 ERROR</H1>\n<H2>The request could not be satisfied.</H2>\n<HR noshade size="1px">\nRequest blocked.\nWe can\'t connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.\n<BR clear="all">\nIf you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.\n<BR clear="all">\n<HR noshade size="1px">\n<PRE>\nGenerated by cloudfront (CloudFront)\nRequest ID: c5ax6a80HaZW_vOW6HqWS_OR_dnWbMRpjZt0on1-FtuGu_ume3yVnQ==\n</PRE>\n<ADDRESS>\n</ADDRESS>\n</BODY></HTML>')

No results anymore after some time of continuous usage

Awesome piece of software! Love your work.

When i query the API in a 30s interval for about 2 days, i am not receiving results anymore.
At that time, with the same account on my phone, from the same network, everything works as expected.

  1. Are you aware of any rate-limiting mechanisms on TGTG/Cloudflare side?
  2. Are you aware of any workarounds for this?
  3. Would it help to run traffic through a proxy with changing IP address?

BR,
Alex

Not able to fetch the items anymore

Hello @ahivert, first of all, thanks for this super handy client. I just wanted to report a problem I encountered while developing a side project.

Basically, when I'm trying to log in, or sometimes when I'm trying to fetch the complete list of items, I get a status code equal to 403 with the following reported HTML:

tgtg.exceptions.TgtgLoginError: (403, b'<html><head><title>apptoogoodtogo.com</title><style>#cmsg{animation: A 1.5s;}@keyframes A{0%{opacity:0;}99%{opacity:0;}100%{opacity:1;}}</style></head><body style="margin:0"><p id="cmsg">Please enable JS and disable any ad blocker</p><script data-cfasync="false">var dd={\'cid\':\'AHrlqAAAAAMAU3Qjyk95ipMA2Wkvag==\',\'hsh\':\'1D42C2CA6131C526E09F294FE96F94\',\'t\':\'fe\',\'r\':\'b\',\'s\':35587,\'e\':\'38fdf8c8806028bc7b010dc826ec31e86f0ac1329c5f86a3deddada3ccde44cf\',\'host\':\'geo.captcha-delivery.com\'}</script><script data-cfasync="false" src="https://ct.captcha-delivery.com/c.js"></script></body></html>\n')

Seems that my account is restricted to some sort of captcha that I don't know how to unlock anymore and it's blocking my requests toward the API. Just once when I tried to open the app to check if the official servers were up I noticed that they were asking me for the captcha confirmation in the app itself but besides that time nothing more...

I have seen that there were multiple opened issues about this specific error but they were almost all related to the login endpoint and never to the items one.

Thanks a lot in advance!

Missing shops

Hi. There are some shops in the app that don't appear when I look for them in my program. Assuming I'm searching correctly, what might be the problem?

Notifications push de panier ajouter dans les favoris

Bonjour est il possible de prévoir une fonction permettant d'avoir une notification immédiate lors de mise à disposition d'un nouveau panier ? (Dans nos favoris par exemple)
Et dans l'idéal est il possible d'ajouter un ou plusieurs de ces paniers de manière automatique afin de les régler lors de la réception de la notification ?

Merci 🙏👌

All requests blocked by cloudflare (403 ERROR: The request could not be satisfied)

Starting today morning at [2021-10-05 04:48:05] UTC i first saw this error message and the problem still exists.

Quick play around with the user agent did not result in anything better.

Python 3.9.1 (v3.9.1:1e5d33e9b9, Dec  7 2020, 12:10:52) 
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import tgtg
>>> client = tgtg.TgtgClient(email='REDACTED', password='REDACTED')
>>> client.get_items()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/winterstein/venv/togoodtogo/lib/python3.9/site-packages/tgtg/__init__.py", line 143, in get_items
    self._login()
  File "/Users/winterstein/venv/togoodtogo/lib/python3.9/site-packages/tgtg/__init__.py", line 122, in _login
    raise TgtgLoginError(response.status_code, response.content)
tgtg.exceptions.TgtgLoginError: (403, b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">\n<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">\n<TITLE>ERROR: The request could not be satisfied</TITLE>\n</HEAD><BODY>\n<H1>403 ERROR</H1>\n<H2>The request could not be satisfied.</H2>\n<HR noshade size="1px">\nRequest blocked.\nWe can\'t connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.\n<BR clear="all">\nIf you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.\n<BR clear="all">\n<HR noshade size="1px">\n<PRE>\nGenerated by cloudfront (CloudFront)\nRequest ID: REDACTED==\n</PRE>\n<ADDRESS>\n</ADDRESS>\n</BODY></HTML>')
>>> 

Can also be verified by running make test with env vars TGTG_EMAIL and TGTG_PASSWORD:

TGTG_EMAIL=REDACTED TGTG_PASSWORD=REDACTED make test
python3 -m poetry run pytest
=========================================================================================================================================================== test session starts ============================================================================================================================================================
platform darwin -- Python 3.9.1, pytest-6.2.3, py-1.10.0, pluggy-0.13.1
rootdir: /Users/winterstein/tmp/tgtg-python, configfile: setup.cfg
plugins: cov-2.12.1, responses-0.4.0
collected 19 items                                                                                                                                                                                                                                                                                                                         

tests/test_api.py FF                                                                                                                                                                                                                                                                                                                 [ 10%]
tests/test_items.py .......                                                                                                                                                                                                                                                                                                          [ 47%]
tests/test_login.py ........                                                                                                                                                                                                                                                                                                         [ 89%]
tests/test_signup.py ..                                                                                                                                                                                                                                                                                                              [100%]

================================================================================================================================================================= FAILURES =================================================================================================================================================================
_____________________________________________________________________________________________________________________________________________________ TestLoginRequired.test_get_items _____________________________________________________________________________________________________________________________________________________

self = <tests.test_api.TestLoginRequired object at 0x7f94a8e0ce80>

    def test_get_items(self):
        client = TgtgClient(
            email=os.environ["TGTG_EMAIL"], password=os.environ["TGTG_PASSWORD"]
        )
>       data = client.get_items(
            favorites_only=False, radius=10, latitude=48.126, longitude=-1.723
        )

tests/test_api.py:20: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
tgtg/__init__.py:143: in get_items
    self._login()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tgtg.TgtgClient object at 0x7f94a8e0cfd0>

    def _login(self):
        if self._already_logged:
            self._refresh_token()
        else:
            if not self.email or not self.password:
                raise ValueError(
                    "You must fill email and password or access_token and user_id"
                )
    
            response = requests.post(
                self._get_url(LOGIN_ENDPOINT),
                headers=self._headers,
                json={
                    "device_type": "ANDROID",
                    "email": self.email,
                    "password": self.password,
                },
                proxies=self.proxies,
                timeout=self.timeout,
            )
            if response.status_code == HTTPStatus.OK:
                login_response = response.json()
                self.access_token = login_response["access_token"]
                self.refresh_token = login_response["refresh_token"]
                self.last_time_token_refreshed = datetime.datetime.now()
                self.user_id = login_response["startup_data"]["user"]["user_id"]
            else:
>               raise TgtgLoginError(response.status_code, response.content)
E               tgtg.exceptions.TgtgLoginError: (403, b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">\n<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">\n<TITLE>ERROR: The request could not be satisfied</TITLE>\n</HEAD><BODY>\n<H1>403 ERROR</H1>\n<H2>The request could not be satisfied.</H2>\n<HR noshade size="1px">\nRequest blocked.\nWe can\'t connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.\n<BR clear="all">\nIf you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.\n<BR clear="all">\n<HR noshade size="1px">\n<PRE>\nGenerated by cloudfront (CloudFront)\nRequest ID: HIqmDLzs--EUvFrZjM1PFLMVxKjlP5H6tUe0xj0uJdLDxtgQZllfvA==\n</PRE>\n<ADDRESS>\n</ADDRESS>\n</BODY></HTML>')

tgtg/__init__.py:122: TgtgLoginError
___________________________________________________________________________________________________________________________________________________ TestLoginRequired.test_get_one_item ____________________________________________________________________________________________________________________________________________________

self = <tests.test_api.TestLoginRequired object at 0x7f94889a6ee0>

    def test_get_one_item(self):
        client = TgtgClient(
            email=os.environ["TGTG_EMAIL"], password=os.environ["TGTG_PASSWORD"]
        )
        item_id = "36684"
>       data = client.get_item(item_id)

tests/test_api.py:32: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
tgtg/__init__.py:176: in get_item
    self._login()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tgtg.TgtgClient object at 0x7f94889a6df0>

    def _login(self):
        if self._already_logged:
            self._refresh_token()
        else:
            if not self.email or not self.password:
                raise ValueError(
                    "You must fill email and password or access_token and user_id"
                )
    
            response = requests.post(
                self._get_url(LOGIN_ENDPOINT),
                headers=self._headers,
                json={
                    "device_type": "ANDROID",
                    "email": self.email,
                    "password": self.password,
                },
                proxies=self.proxies,
                timeout=self.timeout,
            )
            if response.status_code == HTTPStatus.OK:
                login_response = response.json()
                self.access_token = login_response["access_token"]
                self.refresh_token = login_response["refresh_token"]
                self.last_time_token_refreshed = datetime.datetime.now()
                self.user_id = login_response["startup_data"]["user"]["user_id"]
            else:
>               raise TgtgLoginError(response.status_code, response.content)
E               tgtg.exceptions.TgtgLoginError: (403, b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">\n<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">\n<TITLE>ERROR: The request could not be satisfied</TITLE>\n</HEAD><BODY>\n<H1>403 ERROR</H1>\n<H2>The request could not be satisfied.</H2>\n<HR noshade size="1px">\nRequest blocked.\nWe can\'t connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.\n<BR clear="all">\nIf you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.\n<BR clear="all">\n<HR noshade size="1px">\n<PRE>\nGenerated by cloudfront (CloudFront)\nRequest ID: p2OsFEJcRChVWR_TEszPc3j9SB4JKnMP5uCCKl04g4BmpjJOKUZYxA==\n</PRE>\n<ADDRESS>\n</ADDRESS>\n</BODY></HTML>')

tgtg/__init__.py:122: TgtgLoginError
============================================================================================================================================================= warnings summary =============================================================================================================================================================
tests/test_login.py::test_login_empty_token_fail
  /Users/winterstein/tmp/tgtg-python/tgtg/__init__.py:53: UserWarning: 'user_id' is deprecated; use 'email' and 'password'
    warnings.warn("'user_id' is deprecated; use 'email' and 'password'")

tests/test_login.py::test_login_empty_user_id_fail
  /Users/winterstein/tmp/tgtg-python/tgtg/__init__.py:46: UserWarning: 'access_token' is deprecated; use 'email' and 'password'
    warnings.warn("'access_token' is deprecated; use 'email' and 'password'")

-- Docs: https://docs.pytest.org/en/stable/warnings.html

---------- coverage: platform darwin, python 3.9.1-final-0 -----------
Name                 Stmts   Miss Branch BrPart  Cover   Missing
----------------------------------------------------------------
tgtg/__init__.py        93      0     24      0   100%
tgtg/exceptions.py       4      0      0      0   100%
----------------------------------------------------------------
TOTAL                   97      0     24      0   100%

========================================================================================================================================================= short test summary info ==========================================================================================================================================================
FAILED tests/test_api.py::TestLoginRequired::test_get_items - tgtg.exceptions.TgtgLoginError: (403, b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">\n<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">\n<TITLE>ERROR: The request could not b...
FAILED tests/test_api.py::TestLoginRequired::test_get_one_item - tgtg.exceptions.TgtgLoginError: (403, b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">\n<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">\n<TITLE>ERROR: The request could no...
================================================================================================================================================= 2 failed, 17 passed, 2 warnings in 1.22s =================================================================================================================================================
make: *** [test] Error 1

403 error

It appears this method of requesting products is no longer working. A 403 error will occur when trying to connect.

Radius argument for "get_items" seems to have a limit

I have a list of favorite bags which are all located in my city, close to the longitude and latitude I've set in my script.
When calling get_items() with favorites_only=True, it works correctly. However, when calling get_items() with favorites_only=False, there's about 3 of my favorites that show up. I've tried changing the value of the radius argument to really high numbers but it appears that there's a limit.
Do you know if there's a limit with radius argument? While using the app, there are way more stores that show up than when I use get_items with a high radius so this behaviour looks abnormal.

Cannot authenticate anymore (HTTP 403 Forbidden)

Hi,

I cannot authenticate anymore.
Hi,

The following error appends during the client authentication:

Using version 22.9.10
Traceback (most recent call last):
  File "tgtg-alerts.py", line 42, in <module>
    main()
  File "tgtg-alerts.py", line 8, in main
    token = client.get_credentials()
  File "/usr/local/lib/python3.10/dist-packages/tgtg/__init__.py", line 84, in get_credentials
    self.login()
  File "/usr/local/lib/python3.10/dist-packages/tgtg/__init__.py", line 165, in login
    raise TgtgLoginError(response.status_code, response.content)
tgtg.exceptions.TgtgLoginError: (403, b'<html><head><title>apptoogoodtogo.com</title><style>#cmsg{animation: A 1.5s;}@keyframes A{0%{opacity:0;}99%{opacity:0;}100%{opacity:1;}}</style></head><body style="margin:0"><p id="cmsg">Please enable JS and disable any ad blocker</p><script data-cfasync="false">var dd={\'cid\':\'AHrlqAAAAAMA93PIG3AH-zAAVEvhNA==\',\'hsh\':\'1D42C2CA6131C526E09F294FE96F94\',\'t\':\'fe\',\'r\':\'b\',\'s\':35587,\'e\':\'0d8230bb1520a6625b31cd5358fadcfecf8c6a16d692d82b7718b705a2dd3945\',\'host\':\'geo.captcha-delivery.com\'}</script><script data-cfasync="false" src="https://ct.captcha-delivery.com/c.js"></script></body></html>\n')

Remove store / items limit

I'm trying to get items in Milan, but looks like the answer i get is cut / limited in some way.
In the map i see about 200+ stores, while i'm only getting about 15/20 from the api.
Is there a way to get them all?

items = client.get_items(
    favorites_only=False,
    latitude=45.464820,
    longitude=9.188356,
    radius=30,
)

403 Error

Hi,

1st I would like to thank you for the API, it's very convenient

But there is the issue, I have this error :

tgtg.exceptions.TgtgLoginError: (403, b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">\n<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">\n<TITLE>ERROR: The request could not be satisfied</TITLE>\n</HEAD><BODY>\n<H1>403 ERROR</H1>\n<H2>The request could not be satisfied.</H2>\n<HR noshade size="1px">\nRequest blocked.\nWe can\'t connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.\n<BR clear="all">\nIf you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.\n<BR clear="all">\n<HR noshade size="1px">\n<PRE>\nGenerated by cloudfront (CloudFront)\nRequest ID: mnP95UgzM7T7ajdMcSgUAtVsWceXJIwpftw7F5kSQJbq70kcQiFcWg==\n</PRE>\n<ADDRESS>\n</ADDRESS>\n</BODY></HTML>')

I feel like I'm being banned by tgtg, but I can still us my account on their application.
Do you have any on how I could fix that ?

Oh and this error appear with the getItems function but not with TgtgClient(...)

Thank you

Dependency conflict for `requests` between Home Assistant and this module

Hi,

Thanks for this great piece of code. As you may know, I use it to power a Home Assistant integration (here.

One thing bothering me are dependency conflicts. Both Home Assistant and tgtg-python fixes their requests dependencies - here for HA

Today the situation is as follow:

  • Home Assistant Production users needs to use tgtg 0.11.3 as HA require requests==2.27.1
  • Home Assistant Development users needs to use tgtg 0.11.4 as HA require requests==2.28.0

Could we lighten the tgtg requirement to a non-fixed requests dependency?
What is the best way the Python community handle that?

Thanks for your help :)

Unable to create an order

Using version 23.3.1
Traceback (most recent call last):
File "/home/cedric/Dev/Python/Togoodtogo/order.py", line 7, in
order = client.create_order(item_id, number_of_items_to_order)
AttributeError: 'TgtgClient' object has no attribute 'create_order'

I can retrieve items, find 1 item but can't create an order

My code
from tgtg import TgtgClient

client = TgtgClient(access_token="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")

item_id = 550870 # L'ID de l'article que vous souhaitez commander
number_of_items_to_order = 1 # Le nombre d'articles que vous souhaitez commander
order = client.create_order(item_id, number_of_items_to_order)
print(order)

Someone help me please

Cant connect to client anymore

Hello,

just quickly wanted to ask if anyone else is having trouble connecting to the client?

This is how I usually do it:

#################
# run this only once #
#################
# from tgtg import TgtgClient
#
# client = TgtgClient(email="[email protected]")
# credentials = client.get_credentials()
#
# # go to email and verify
# import json
# # save credentials to disk
# with open("/home/pi/projects/tgtg_access/credentials.json", "w") as f:
#     json.dump(credentials, f, indent=4)
#

with open("/home/pi/projects/tgtg_access/credentials.json", "r") as f:
    credentials = json.load(f)

client = TgtgClient(access_token=credentials["access_token"],
                    refresh_token=credentials["refresh_token"],
                    user_id=credentials["user_id"])
print("Connected to client")

For some reason, it just never connects... I have used the same code for the last months and never had an issue. Do my credentials expire after a certain time?

Any help is much appreciated.

credentials not shown

it is not possible to recive the credentials, got empty response

`Check your mailbox on PC to continue... (Mailbox on mobile won't work, if you have installed tgtg app.)
Check your mailbox on PC to continue... (Mailbox on mobile won't work, if you have installed tgtg app.)
Logged in!

rx@lappy ~/tgtg
$
`

Refresh token happens every call, wrong order in subtraction

As title states - the refresh token happens every call due to wrong order of variables in subtraction - we really should expect last_time_token_refreshed to be before now(), so it should the other way around.
No idea if this is python version specific / timezone specifi (and 'works here') or was just overlooked :)

Not PR'ing because I have no idea how your flow for new version works and where I need to bump version number or so :)

            or (self.last_time_token_refreshed - datetime.datetime.now()).seconds
            <= self.access_token_lifetime

https://github.com/ahivert/tgtg-python/blob/master/tgtg/__init__.py#L75

import datetime

now = datetime.datetime.now()
last_time_token_refreshed = now-datetime.timedelta(seconds=20)
print( (now-last_time_token_refreshed).seconds )
print( (last_time_token_refreshed-now).seconds )
20
86380

Please enable JS and disable any ad blocker

Getting a 403 and this error when trying to get_credentials()

raise TgtgLoginError(response.status_code, response.content)
tgtg.exceptions.TgtgLoginError: (403, b'<title>apptoogoodtogo.com</title><style>#cmsg{animation: A 1.5s;}@Keyframes A{0%{opacity:0;}99%{opacity:0;}100%{opacity:1;}}</style>

Please enable JS and disable any ad blocker

<script>var dd={'cid':'AHrlqAAAAAMAMS16IrlzdfsAwCl9_Q==','hsh':'1D42C2CA6131C526E09F294FE96F94','t':'fe','r':'b','s':35693,'host':'geo.captcha-delivery.com'}</script><script src="https://ct.captcha-delivery.com/c.js"></script>\n')

Any ideas?

Auth api not working

Looks like 'auth/v1/loginByEmail' either stopped working or is down temporarily. Getting 403 responses.

store_id

Hi, notice that some stores have a alfanumeric character :

example
image

but the get_item() does not seem to accept this
Am I missing something?

Please enable JS and disable any ad blocker

Hi,

Since a few days / weeks I'm receiving an error.
Every 10 minutes I'm checking if there are new items available in tgtg.
3 accounts are used in the code, it changes frequently who's getting the error, sometimes 1 account, sometimes 2 of them.

The error message:
(403, b'<html><head><title>apptoogoodtogo.com</title><style>#cmsg{animation: A 1.5s;}@keyframes A{0%{opacity:0;}99%{opacity:0;}100%{opacity:1;}}</style></head><body style="margin:0"><p id="cmsg">Please enable JS and disable any ad blocker</p><script>var dd={\'cid\':\'AHrlqAAAAAMAcU7SCObNTwsBKgIYEaQFwwACETL__ih5Nw==\',\'hsh\':\'1D42C2CA6131C526E09F294FE96F94\',\'t\':\'fe\',\'r\':\'b\',\'s\':35560,\'host\':\'geo.captcha-delivery.com\'}</script><script src="https://ct.captcha-delivery.com/c.js"></script></body></html>\n')

Datadom captcha

Hello,

I am facing similar issue to the #193
The problem is in fact that Too Good Too Go added Datadome as an anti bot system.

Whatever I do:

  • changing IP adress
  • changing machine
  • changing account
  • changing user agent, or other parts of headers

All the time I am facing the same 403 error with a html page as a response ... something geo-captcha something. The interesting fact is that Datadome can somehow detect my machine from which I am running script with my account credentials and block it, while the same account works without any problems on my phone.

<html>

<head>
    <title>apptoogoodtogo.com</title>
    <style>
      #cmsg {
        animation: A 1.5s;
      }

      @keyframes A {
        0% {
          opacity: 0;
        }

        99% {
          opacity: 0;
        }

        100% {
          opacity: 1;
        }
      }
    </style>
</head>

<body style="margin:0">
<p id="cmsg">Please enable JS and disable any ad blocker</p>
<script data-cfasync="false">
var dd={'cid':(some ids which i am afriad to provide)==','hsh':(some ids which i am afriad to provide),'t':'bv','r':'b','s':(some ids which i am afriad to provide),'e':(some ids which i am afriad to provide),'host':'geo.captcha-delivery.com'}
</script>
<script data-cfasync="false" src="https://ct.captcha-delivery.com/c.js"></script>
</body>

</html>

So yeah... The question is, does anyone have any idea how to bypass Datadome? Or is it even possible in any way?

tgtg-notifier.com redirects to fishy websites

I noticed the URL tgtg-notifier.com redirects to some fishy websites. I don't know if the tgtg-notifier is related to this project. Maybe the link should be removed from this repo?

Apart from that: the lib works like a charm so far. Awesome job! I like the filter options possible with get_items.

ERROR: Please enable JS and disable any ad blocker

Hi
When I run the example code:

from tgtg import TgtgClient

client = TgtgClient(email="MY_EMAIL")
credentials = client.get_credentials()
print(credentials)

I get the following error:

Using version 22.9.3
Traceback (most recent call last):
  File "c:\Users\noahe\Documents\New folder\TooGoodToGoAPI\test.py", line 4, in <module>
    credentials = client.get_credentials()
  File "C:\Users\noahe\AppData\Local\Programs\Python\Python310\lib\site-packages\tgtg\__init__.py", line 84, in get_credentials
    self.login()
  File "C:\Users\noahe\AppData\Local\Programs\Python\Python310\lib\site-packages\tgtg\__init__.py", line 165, in login
    raise TgtgLoginError(response.status_code, response.content)
tgtg.exceptions.TgtgLoginError: (403, b'<html><head><title>apptoogoodtogo.com</title><style>#cmsg{animation: A 1.5s;}@keyframes A{0%{opacity:0;}99%{opacity:0;}100%{opacity:1;}}</style></head><body style="margin:0"><p id="cmsg">Please enable JS and disable any ad blocker</p><script data-cfasync="false">var dd={\'cid\':\'AHrlqAAAAAMAW2DhqabU-kkAVMKRdQ==\',\'hsh\':\'1D42C2CA6131C526E09F294FE96F94\',\'t\':\'fe\',\'r\':\'b\',\'s\':35587,\'e\':\'909f1806ea8d443b7bc63b6fd4c1b2cdc65e35d28d26aafcface1c77b0d34a0f\',\'host\':\'geo.captcha-delivery.com\'}</script><script data-cfasync="false" src="https://ct.captcha-delivery.com/c.js"></script></body></html>\n')

"Please enable JS and disable any ad blocker"/"ERROR 403" seems to be a default error if an invalid request has been detected. (https://www.reddit.com/r/learnjavascript/comments/skbokf/fetch_request_from_server_to_web_api_returns/)

Is it possible that a recent API change on their part broke the code?
Thanks for looking into it!

EDIT:
I instantly get the error and don't receive an email.

Sign Up isn't working

When I try to signup with python I have this error:
client = TgtgClient(email="[email protected]", password="password", name="name")
TypeError: init() got an unexpected keyword argument 'name'

get_item() doesnt return anything after hours of continuous use

First of all, thanks for this brilliant and straight forward package.
I have been playing around with it for some time now and seem to run into a strange situation and was wondering if anyone might have experienced something similar.

I am running a script which checks a bunch of stores every 20 seconds if any item becomes available and if it does, it sends me an email. In between each API call, I add a delay of 2 seconds to make sure not too send to many requests. This works great for the first ~15 hours. However, at some point client.get_item() simply doesnt return anything. Not a proper response nor an error. Just nothing and the "code gets stuck". I was unable to reliably reproduce the issue, but it occurs every time after running it for a long time.

Did anyone experience something similar yet?

Any help is much appreciated!

Display of the version used

Hello

Would it be possible to have a parameter to avoid the following display at the beginning of the program:
Using version 23.3.11

Thanks in advance

403 error

Hi,
I am running tgtg in Home Assistant and am getting:

Source: custom_components/tgtg/sensor.py:108
Integration: Sensor (documentation, issues)
First occurred: December 13, 2021, 3:10:12 AM (1 occurrences)
Last logged: December 13, 2021, 3:10:12 AM

Error while setting up tgtg platform for sensor
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/entity_platform.py", line 249, in _async_setup_platform
    await asyncio.shield(task)
  File "/usr/local/lib/python3.9/concurrent/futures/thread.py", line 52, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/config/custom_components/tgtg/sensor.py", line 58, in setup_platform
    add_entities([TGTGSensor(each_item_id)])
  File "/config/custom_components/tgtg/sensor.py", line 76, in __init__
    self.update()
  File "/config/custom_components/tgtg/sensor.py", line 108, in update
    tgtg_answer = tgtg_client.get_item(item_id=self.item_id)
  File "/usr/local/lib/python3.9/site-packages/tgtg/__init__.py", line 238, in get_item
    self.login()
  File "/usr/local/lib/python3.9/site-packages/tgtg/__init__.py", line 119, in login
    self._refresh_token()
  File "/usr/local/lib/python3.9/site-packages/tgtg/__init__.py", line 109, in _refresh_token
    raise TgtgAPIError(response.status_code, response.content)
tgtg.exceptions.TgtgAPIError: (403, b'<html><head><title>apptoogoodtogo.com</title><style>#cmsg{animation: A 1.5s;}@keyframes A{0%{opacity:0;}99%{opacity:0;}100%{opacity:1;}}</style></head><body style="margin:0"><p id="cmsg">Please enable JS and disable any ad blocker</p><script>var dd={\'cid\':\'AHrlqAAAAAMA7epZgDR-8Y8BKhA3gReKAABmQr8Yv76uRg==\',\'hsh\':\'1D42C2CA6131C526E09F294FE96F94\',\'t\':\'fe\',\'r\':\'b\',\'s\':35560,\'host\':\'geo.captcha-delivery.com\'}</script><script src="https://ct.captcha-delivery.com/c.js"></script></body></html>\n')

I also reported this problem here: Chouffy/home_assistant_tgtg#13 but was forwarded to this repo.
Can someone help me get it to work again?

401 Unauthorized Error when using the latest version

I am using the latest version that queries Google Play and gets the latest version number, and it still logs in successfully, but I'm seeing the following error when using any of the other API methods.

TgtgAPIError: (401, b'{"timestamp":1655951233381,"status":401,"error":"Unauthorized","path":"/api/item/v7/1"}')

I verified that the authorization is put into the header. I have also tried using a different account.

Does anyone else have a similar issue?

Thank you!

Client doesn't print credentials

Thank you very much for designing this piece of software! It's absolutely a great help.

I did notice one thing that bugged me. The readme states that after running the get_credentials() function, the client will print the needed tokens. This didn't work for me, so i took a quick look at the source code and saw that the function only returned the tokens, not print them.

Could you change this in the Readme please? This way, someone with less knowledge of python could still use the library and not get frustrated and give up their (maybe) first Python project. Thanks!

TgtgPollingError: Please accept terms first, validate your email and then retry!

I am using the latest version (0.8.0) and when following the instructions (I tried both client.get_credentials() and client.login()) I get the following error:

./tgtg/__init__.py in login(self)
    127                 first_login_response = response.json()
    128                 if first_login_response["state"] == "TERMS":
--> 129                     raise TgtgPollingError(
    130                         "Please accept terms first, validate your email and then retry!"
    131                     )

TgtgPollingError: Please accept terms first, validate your email and then retry!

Everything is working fine with the app, I tried to log out, uninstall, etc. I get the mail from the app, never from the python script.

What is the way to accept the terms within tgtg-python?

How to get the time a store sold out?

Does the library also provide the functionality to get the time when a store is sold out? Like "sold out since yesterday 20:00"
I tried to find it in the response of get_item, but wasn't able to find anything like it

cannot import name 'TgtgClient' from partially initialized module

When trying to install pip install tgtg I get:

$ pip install tgtg
Collecting tgtg
Using cached tgtg-0.11.0-py3-none-any.whl (6.9 kB)
Requirement already satisfied: requests==2.26.0 in .............
Requirement already satisfied: urllib3<1.27,>=1.21.1 in ..........
Requirement already satisfied: idna<4,>=2.5 in c ............

It looks like it doens't install because when running the token-script I get:
ImportError: cannot import name 'TgtgClient' from partially initialized module 'tgtg' (most likely due to a circular import)

TgtgLoginError - Seems like two factor auth is required // Error 403

First seen on a mobile a few days ago, now happening to me here too:

>>> from tgtg import TgtgClient
>>> client = TgtgClient(email="REDACTED", password="REDACTED")
>>> client.get_items()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/winterstein/venv/togoodtogo/lib/python3.9/site-packages/tgtg/__init__.py", line 143, in get_items
    self._login()
  File "/Users/winterstein/venv/togoodtogo/lib/python3.9/site-packages/tgtg/__init__.py", line 122, in _login
    raise TgtgLoginError(response.status_code, response.content)
tgtg.exceptions.TgtgLoginError: (403, b'<html><head><title>apptoogoodtogo.com</title><style>#cmsg{animation: A 1.5s;}@keyframes A{0%{opacity:0;}99%{opacity:0;}100%{opacity:1;}}</style></head><body style="margin:0"><p id="cmsg">Please enable JS and disable any ad blocker</p><script>var dd={\'cid\':\'REDACTED==\',\'hsh\':\'REDACTED\',\'t\':\'fe\',\'r\':\'b\',\'s\':35693,\'host\':\'geo.captcha-delivery.com\'}</script><script src="https://ct.captcha-delivery.com/c.js"></script></body></html>\n')

On mobile, an email is sent with a 6-digits code that needs to be entered into the app, or a link that opens up the app.

Problem with the range

I’m writing and testing a script and I realized that probably there’s a problem with the radius parameter in the client.get_items function. I tried many times to change the radius, from 10 to 15, then to 30 but the results are always the same even if in a larger radius lots of more shops are currently active. I don’t understand if the problem is in my code or in the library. If I change the other parameters (latitude and longitude) everything works as expected.

Login with refresh_token not possible

Script:

from tgtg import TgtgClient

client = TgtgClient(email="<your_email>", timeout=30)
client.login()

access_token = client.access_token
refresh_token = client.refresh_token
user_id = client.user_id
client = TgtgClient(access_token=access_token, refresh_token=refresh_token, user_id=user_id)
client.login()

Result of the last client.login() call:

Traceback (most recent call last):
  File "script.py", line 10, in <module>
    client.login()
  File "/home/me/.local/lib/python3.8/site-packages/tgtg/__init__.py", line 112, in login
    self._refresh_token()
  File "/home/me/.local/lib/python3.8/site-packages/tgtg/__init__.py", line 98, in _refresh_token
    raise TgtgAPIError(response.status_code, response.content)
tgtg.exceptions.TgtgAPIError: (404, b'{"timestamp":1637700413860,"status":404,"error":"Not Found","path":"/api/auth/v1/token/refresh"}')

The endpoint is still used in current master:

REFRESH_ENDPOINT = "auth/v1/token/refresh"

How to retreive access token

It seems the access_token property is not set on the client object.

This is causing the user to receive a login email every time the script runs.

error 404

Hello,

I am already a user of tgtg bot but from anotehr repo. Unfortunatly it stopped working due to login changes. I was looking then for another repo that did the same and came to yourse.
I installed everything correctly, but when launching to get my favorite items i am stuck with a 404 error on login. I've seen that everyone is getting 403 error but for me it is a 404. Did I something wrong or ? ...

image
image

400 error on login

Hi! I have some problems trying to login using email/pass. I am gettings 400 error with

Bad request. We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner. 
If you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation. 

Does it mean I somehow was banned by cloudfront? I tried different PCs with different IPs and user-agents and email/password pairs. Still the same.

Access once

Is there a way in which I can access with the same credentials every time I run my script? (And not get the "We noticed you accessed from a new device" mail from too good to go every time?)

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.