{"id":13458932,"url":"https://github.com/scrapinghub/andi","last_synced_at":"2025-04-10T00:19:46.567Z","repository":{"id":52492911,"uuid":"204053592","full_name":"scrapinghub/andi","owner":"scrapinghub","description":"Library for annotation-based dependency injection","archived":false,"fork":false,"pushed_at":"2025-02-07T17:16:18.000Z","size":270,"stargazers_count":22,"open_issues_count":5,"forks_count":5,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-04-02T22:09:16.153Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/scrapinghub.png","metadata":{"files":{"readme":"README.rst","changelog":"CHANGES.rst","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-08-23T18:50:29.000Z","updated_at":"2025-02-05T16:14:24.000Z","dependencies_parsed_at":"2024-10-29T04:30:29.361Z","dependency_job_id":"0e9a10d9-1859-4032-99d2-a5a101c2d61a","html_url":"https://github.com/scrapinghub/andi","commit_stats":{"total_commits":149,"total_committers":4,"mean_commits":37.25,"dds":"0.30201342281879195","last_synced_commit":"fc10827a955ae218d83eb204fa1847c43aff1005"},"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/scrapinghub%2Fandi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/scrapinghub%2Fandi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/scrapinghub%2Fandi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/scrapinghub%2Fandi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/scrapinghub","download_url":"https://codeload.github.com/scrapinghub/andi/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248131645,"owners_count":21052890,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T09:00:59.918Z","updated_at":"2025-04-10T00:19:46.536Z","avatar_url":"https://github.com/scrapinghub.png","language":"Python","funding_links":[],"categories":["Software"],"sub_categories":["DI Frameworks / Containers"],"readme":"====\nandi\n====\n\n.. image:: https://img.shields.io/pypi/v/andi.svg\n   :target: https://pypi.python.org/pypi/andi\n   :alt: PyPI Version\n\n.. image:: https://img.shields.io/pypi/pyversions/andi.svg\n   :target: https://pypi.python.org/pypi/andi\n   :alt: Supported Python Versions\n\n.. image:: https://github.com/scrapinghub/andi/workflows/tox/badge.svg\n   :target: https://github.com/scrapinghub/andi/actions\n   :alt: Build Status\n\n.. image:: https://codecov.io/github/scrapinghub/andi/coverage.svg?branch=master\n   :target: https://codecov.io/gh/scrapinghub/andi\n   :alt: Coverage report\n\n``andi`` makes easy implementing custom dependency injection mechanisms\nwhere dependencies are expressed using type annotations.\n\n``andi`` is useful as a building block for frameworks, or as a library\nwhich helps to implement dependency injection (thus the name -\nANnotation-based Dependency Injection).\n\nLicense is BSD 3-clause.\n\nInstallation\n============\n\n::\n\n    pip install andi\n\nandi requires Python \u003e= 3.9.\n\nGoal\n====\n\nSee the following classes that represents parts of a car\n(and the car itself):\n\n.. code-block:: python\n\n    class Valves:\n        pass\n\n    class Engine:\n        def __init__(self, valves):\n            self.valves = valves\n\n    class Wheels:\n        pass\n\n    class Car:\n        def __init__(self, engine, wheels):\n            self.engine = engine\n            self.wheels = wheels\n\nThe following would be the usual way of build a ``Car`` instance:\n\n.. code-block:: python\n\n    valves = Valves()\n    engine = Engine(valves)\n    wheels = Wheels()\n    car = Car(engine, wheels)\n\nThere are some dependencies between the classes: A car requires\nand engine and wheels to be built, as well as the engine requires\nvalves. These are the car dependencies and sub-dependencies.\n\nThe question is, could we have an automatic way of building instances?\nFor example, could we have a ``build`` function that\ngiven the ``Car`` class or any other class would return an instance\neven if the class itself has some other dependencies?\n\n.. code-block:: python\n\n    car = build(Car)  # Andi helps creating this generic build function\n\n``andi`` inspect the dependency tree and creates a plan making easy creating\nsuch a ``build`` function.\n\nThis is how this plan for the ``Car`` class would looks like:\n\n1. Invoke ``Valves`` with empty arguments\n2. Invoke ``Engine`` using the instance created in 1 as the argument ``valves``\n3. Invoke ``Wheels`` with empty arguments\n4. Invoke ``Cars`` with the instance created in 2 as the ``engine`` argument and with\n   the instance created in 3 as the ``wheels`` argument\n\nType annotations\n----------------\n\nBut there is a missing piece in the Car example before. How can\n``andi`` know that the class ``Valves`` is required to build the\nargument ``valves``? A first idea would be to use the argument\nname as a hint for the class name\n(as `pinject \u003chttps://pypi.org/project/pinject/\u003e`_ does),\nbut ``andi`` opts to rely on arguments' type annotations instead.\n\nThe classes for ``Car`` should then be rewritten as:\n\n.. code-block:: python\n\n    class Valves:\n        pass\n\n    class Engine:\n        def __init__(self, valves: Valves):\n            self.valves = valves\n\n    class Wheels:\n        pass\n\n    class Car:\n        def __init__(self, engine: Engine, wheels: Wheels):\n            self.engine = engine\n            self.wheels = wheels\n\nNote how now there is a explicit annotation stating that the\n``valves`` argument is of type ``Valves``\n(same for ``engine`` and ``wheels``).\n\nThe ``andi.plan`` function can now create a plan to build the\n``Car`` class (ignore the ``is_injectable`` parameter by now):\n\n.. code-block:: python\n\n    plan = andi.plan(Car, is_injectable={Engine, Wheels, Valves})\n\n\nThis is what the ``plan`` variable contains:\n\n.. code-block:: python\n\n    [(Valves, {}),\n     (Engine, {'valves': Valves}),\n     (Wheels, {}),\n     (Car,    {'engine': Engine,\n               'wheels': Wheels})]\n\nNote how this plan correspond exactly to the 4-steps plan described\nin the previous section.\n\nBuilding from the plan\n----------------------\n\nCreating a generic function to build the instances from\na plan generated by ``andi`` is then very easy:\n\n.. code-block:: python\n\n    def build(plan):\n        instances = {}\n        for fn_or_cls, kwargs_spec in plan:\n            instances[fn_or_cls] = fn_or_cls(**kwargs_spec.kwargs(instances))\n        return instances\n\nSo let's see putting all the pieces together. The following code\ncreates an instance of ``Car`` using ``andi``:\n\n.. code-block:: python\n\n    plan = andi.plan(Car, is_injectable={Engine, Wheels, Valves})\n    instances = build(plan)\n    car = instances[Car]\n\nis_injectable\n-------------\n\nIt is not always desired for ``andi`` to manage every single annotation found.\nInstead is usually better to explicitly declare which types\ncan be handled by ``andi``. The argument ``is_injectable``\nallows to customize this feature.\n\n``andi`` will raise an error on the presence of a dependency that cannot be resolved\nbecause it is not injectable.\n\nUsually is desirable to declare injectabilty by\ncreating a base class to inherit from. For example,\nwe could create a base class ``Injectable`` as base\nclass for the car components:\n\n.. code-block:: python\n\n    class Injectable(ABC):\n        pass\n\n    class Valves(Injectable):\n        pass\n\n    class Engine(Injectable):\n        def __init__(self, valves: Valves):\n            self.valves = valves\n\n    class Wheels(Injectable):\n        pass\n\nThe call to ``andi.plan`` would then be:\n\n.. code-block:: python\n\n    is_injectable = lambda cls: issubclass(cls, Injectable)\n    plan = andi.plan(Car, is_injectable=is_injectable)\n\nFunctions and methods\n---------------------\n\nDependency injection is also very useful when applied to functions.\nImagine that you have a function ``drive`` that drives the ``Car``\nthrough the ``Road``:\n\n.. code-block:: python\n\n    class Road(Injectable):\n        ...\n\n    def drive(car: Car, road: Road, speed):\n        ... # Drive the car through the road\n\nThe dependencies has to be resolved before invoking\nthe ``drive`` function:\n\n.. code-block:: python\n\n    plan = andi.plan(drive, is_injectable=is_injectable)\n    instances = build(plan.dependencies)\n\nNow the ``drive`` function can be invoked:\n\n.. code-block:: python\n\n    drive(instances[Car], instances[Road], 100)\n\nNote that ``speed`` argument was not annotated. The resultant plan just won't include it\nbecause the ``andi.plan`` ``full_final_kwargs`` parameter is ``False``\nby default. Otherwise, an exception would have been raised (see ``full_final_kwargs`` argument\ndocumentation for more information).\n\nAn alternative and more generic way to invoke the drive function\nwould be:\n\n.. code-block:: python\n\n    drive(speed=100, **plan.final_kwargs(instances))\n\ndataclasses and attrs\n---------------------\n\n``andi`` supports classes defined using `attrs \u003chttps://www.attrs.org/\u003e`_\nand also `dataclasses \u003chttps://docs.python.org/3/library/dataclasses.html\u003e`_.\nFor example the ``Car`` class could have been defined as:\n\n.. code-block:: python\n\n    # attrs class example\n    @attr.s(auto_attribs=True)\n    class Car:\n        engine: Engine\n        wheels: Wheels\n\n    # dataclass example\n    @dataclass\n    class Car(Injectable):\n        engine: Engine\n        wheels: Wheels\n\nUsing ``attrs`` or ``dataclass`` is handy because they avoid\nsome boilerplate.\n\nExternally provided dependencies\n--------------------------------\n\nRetaining the control over object instantiation\ncould be desired in some cases. For example creating\na database connection could require accessing some\ncredentials registry or getting the connection from a pool\nso you might want to control building\nsuch instances outside of the regular\ndependency injection mechanism.\n\n``andi.plan`` allows to specify which types would be\nexternally provided. Let's see an example:\n\n.. code-block:: python\n\n    class DBConnection(ABC):\n\n        @abstractmethod\n        def getConn():\n            pass\n\n    @dataclass\n    class UsersDAO:\n        conn: DBConnection\n\n        def getUsers():\n           return self.conn.query(\"SELECT * FROM USERS\")\n\n``UsersDAO`` requires a database connection to run queries.\nBut the connection will be provided externally from a pool, so we\ncall then ``andi.plan`` using also the ``externally_provided``\nparameter:\n\n.. code-block:: python\n\n    plan = andi.plan(UsersDAO, is_injectable=is_injectable,\n                     externally_provided={DBConnection})\n\nThe build method should then be modified slightly to be able\nto inject externally provided instances:\n\n.. code-block:: python\n\n    def build(plan, instances_stock=None):\n        instances_stock = instances_stock or {}\n        instances = {}\n        for fn_or_cls, kwargs_spec in plan:\n            if fn_or_cls in instances_stock:\n                instances[fn_or_cls] = instances_stock[fn_or_cls]\n            else:\n                instances[fn_or_cls] = fn_or_cls(**kwargs_spec.kwargs(instances))\n        return instances\n\nNow we are ready to create ``UserDAO`` instances with ``andi``:\n\n.. code-block:: python\n\n    plan = andi.plan(UsersDAO, is_injectable=is_injectable,\n                     externally_provided={DBConnection})\n    dbconnection = DBPool.get_connection()\n    instances = build(plan.dependencies, {DBConnection: dbconnection})\n    users_dao = instances[UsersDAO]\n    users = user_dao.getUsers()\n\nNote that being injectable is not required for externally provided\ndependencies.\n\nOptional\n--------\n\n``Optional`` type annotations can be used in case of\ndependencies that can be optional. For example:\n\n.. code-block:: python\n\n    @dataclass\n    class Dashboard:\n        conn: Optional[DBConnection]\n\n        def showPage():\n            if self.conn:\n                self.conn.query(\"INSERT INTO VISITS ...\")\n            ...  # renders a HTML page\n\nIn this example, the ``Dashboard`` class generates a HTML page to be served, and\nalso stores the number of visits into a database. Database\ncould be absent in some environments, but you might want\nthe dashboard to work even if it cannot log the visits.\n\nWhen a database connection is possible the plan call would be:\n\n.. code-block:: python\n\n    plan = andi.plan(UsersDAO, is_injectable=is_injectable,\n                     externally_provided={DBConnection})\n\n\nAnd the following when the connection is absent:\n\n.. code-block:: python\n\n    plan = andi.plan(UsersDAO, is_injectable=is_injectable,\n                     externally_provided={})\n\nIt is also required to register the type of ``None``\nas injectable. Otherwise ``andi.plan`` with raise an exception\nsaying that \"NoneType is not injectable\".\n\n.. code-block:: python\n\n    Injectable.register(type(None))\n\nUnion\n-----\n\n``Union`` can also be used to express alternatives. For example:\n\n.. code-block:: python\n\n    @dataclass\n    class UsersDAO:\n        conn: Union[ProductionDBConnection, DevelopmentDBConnection]\n\n``DevelopmentDBConnection`` will be injected in the absence of\n``ProductionDBConnection``.\n\nAnnotated\n---------\n\n``Annotated`` type annotations can be used to attach arbitrary metadata that\nwill be preserved in the plan. Occurrences of the same type annotated with\ndifferent metadata will not be considered duplicates. For example:\n\n.. code-block:: python\n\n    @dataclass\n    class Dashboard:\n        conn_main: Annotated[DBConnection, \"main DB\"]\n        conn_stats: Annotated[DBConnection, \"stats DB\"]\n\nThe plan will contain both dependencies.\n\nCustom builders\n---------------\n\nSometimes a dependency can't be created directly but needs some additional code\nto be built. And that code can also have its own dependencies:\n\n.. code-block:: python\n\n    class Wheels:\n        pass\n\n    def wheel_factory(wheel_builder: WheelBuilder) -\u003e Wheels:\n        return wheel_builder.get_wheels()\n\nAs by default ``andi`` can't know how to create a ``Wheels`` instance or that\nthe plan needs to create a ``WheelBuilder`` instance first, it needs to be told\nthis with a ``custom_builder_fn`` argument:\n\n.. code-block:: python\n\n    custom_builders = {\n        Wheels: wheel_factory,\n    }\n\n    plan = andi.plan(Car, is_injectable={Engine, Wheels, Valves},\n                     custom_builder_fn=custom_builders.get,\n                     )\n\n``custom_builder_fn`` should be a function that takes a type and returns a factory\nfor that type.\n\nThe build code also needs to know how to build ``Wheels`` instances. A plan step\nfor an object built with a custom builder uses an instance of the ``andi.CustomBuilder``\nwrapper that contains the type to be built in the ``result_class_or_fn`` attribute and\nthe callable for building it in the ``factory`` attribute:\n\n.. code-block:: python\n\n    from andi import CustomBuilder\n\n    def build(plan):\n        instances = {}\n        for fn_or_cls, kwargs_spec in plan:\n            if isinstance(fn_or_cls, CustomBuilder):\n                instances[fn_or_cls.result_class_or_fn] = fn_or_cls.factory(**kwargs_spec.kwargs(instances))\n            else:\n                instances[fn_or_cls] = fn_or_cls(**kwargs_spec.kwargs(instances))\n        return instances\n\nFull final kwargs mode\n-------------------------\n\nBy default ``andi.plan`` won't fail if it is not able to provide\nsome of the direct dependencies for the given input (see the\n``speed`` argument in one of the examples above).\n\nThis behaviour is desired when inspecting functions\nfor which is already known that some arguments won't be\ninjectable but they will be provided by other means\n(like the ``drive`` function above).\n\nBut in other cases is better to be sure that all dependencies\nare fulfilled and otherwise fail. Such is the case for classes.\nSo it is recommended to set ``full_final_kwargs=True`` when invoking\n``andi.plan`` for classes.\n\nOverrides\n---------\n\nLet's go back to the ``Car`` example. Imagine you want to build a car again.\nBut this time you want to replace the ``Engine`` because this is\ngoing to be an electric car!. And of course, an electric engine contains a battery\nand have no valves at all. This could be the new ``Engine``:\n\n.. code-block:: python\n\n    class Battery:\n        pass\n\n    class ElectricEngine(Engine):\n\n        def __init__(self, battery: Battery):\n            self.battery = valves\n\nAndi offers the possibility to replace dependencies when planning,\nand this is what is required to build the electric car: we need\nto replace any dependency on ``Engine`` by a dependency on ``ElectricEngine``.\nThis is exactly what overrides offers. Let's see how ``plan`` should\nbe invoked in this case:\n\n.. code-block:: python\n\n    plan = andi.plan(Car, is_injectable=is_injectable,\n                     overrides={Engine: ElectricEngine}.get)\n\nNote that Andi will unroll the new dependencies properly. That is,\n``Valves`` and ``Engine`` won't be in the resultant plan but\n``ElectricEngine`` and ``Battery`` will.\n\nIn summary, overrides offers a way to override the default\ndependencies anywhere in the tree, changing them with an\nalternative one.\n\nBy default overrides are not recursive: overrides aren't applied\nover the children of an already overridden dependency. There\nis flag to turn recursion on if this is what is desired.\nCheck ``andi.plan`` documentation for more information.\n\nWhy type annotations?\n---------------------\n\n``andi`` uses type annotations to declare dependencies (inputs).\nIt has several advantages, and some limitations as well.\n\nAdvantages:\n\n1. Built-in language feature.\n2. You're not lying when specifying a type - these\n   annotations still work as usual type annotations.\n3. In many projects you'd annotate arguments anyways, so ``andi`` support\n   is \"for free\".\n\nLimitations:\n\n1. Callable can't have two arguments of the same type.\n2. This feature could possibly conflict with regular type annotation usages.\n\nIf your callable has two arguments of the same type, consider making them\ndifferent types. For example, a callable may receive url and html of\na web page:\n\n.. code-block:: python\n\n    def parse(html: str, url: str):\n        # ...\n\nTo make it play well with ``andi``, you may define separate types for url\nand for html:\n\n.. code-block:: python\n\n    class HTML(str):\n        pass\n\n    class URL(str):\n        pass\n\n    def parse(html: HTML, url: URL):\n        # ...\n\nThis is more boilerplate though.\n\nWhy doesn't andi handle creation of objects?\n--------------------------------------------\n\nCurrently ``andi`` just inspects callable and chooses best concrete types\na framework needs to create and pass to a callable, without prescribing how\nto create them. This makes ``andi`` useful in various contexts - e.g.\n\n* creation of some objects may require asynchronous functions, and it\n  may depend on libraries used (asyncio, twisted, etc.)\n* in streaming architectures (e.g. based on Kafka) inspection may happen\n  on one machine, while creation of objects may happen on different nodes\n  in a distributed system, and then actually running a callable may happen on\n  yet another machine.\n\nIt is hard to design API with enough flexibility for all such use cases.\nThat said, ``andi`` may provide more helpers in future,\nonce patterns emerge, even if they're useful only in certain contexts.\n\nExamples: callback based frameworks\n-----------------------------------\n\nSpider example\n**************\n\nNothing better than a example to understand how ``andi`` can be useful.\nLet's imagine you want to implemented a callback based framework\nfor writing spiders to crawl web pages.\n\nThe basic idea is that there is framework in which the user\ncan write spiders. Each spider is a collection of callbacks\nthat can process data from a page, emit extracted data or request new\npages. Then, there is an engine that takes care of downloading\nthe web pages\nand invoking the user defined callbacks, chaining requests\nwith its corresponding callback.\n\nLet's see an example of an spider to download recipes\nfrom a cooking page:\n\n.. code-block:: python\n\n    class MySpider(Spider):\n        start_url = \"htttp://a_page_with_a_list_of_recipes\"\n\n        def parse(self, response):\n            for url in recipes_urls_from_page(response)\n                yield Request(url, callback=parse_recipe)\n\n        def parse_recipe(self, response):\n            yield extract_recipe(response)\n\n\nIt would be handy if the user can define some requirements\njust by annotating parameters in the callbacks. And ``andi`` make it\npossible.\n\nFor example, a particular callback could require access to the cookies:\n\n.. code-block:: python\n\n    def parse(self, response: Response, cookies: CookieJar):\n        # ... Do something with the response and the cookies\n\nIn this case, the engine can use ``andi`` to inspect the ``parse`` method, and\ndetect that ``Response`` and ``CookieJar`` are required.\nThen the framework will build them and will invoke the callback.\n\nThis functionality would serve to inject into the users callbacks\nsome components only when they are required.\n\nIt could also serve to encapsulate better the user code. For\nexample, we could just decouple the recipe extraction into\nit's own class:\n\n.. code-block:: python\n\n    @dataclass\n    class RecipeExtractor:\n        response: Response\n\n        def to_item():\n            return extract_recipe(self.response)\n\nThe callback could then be defined as:\n\n.. code-block:: python\n\n        def parse_recipe(extractor: RecipeExtractor):\n            yield extractor.to_item()\n\nNote how handy is that with ``andi`` the engine can create\nan instance of ``RecipesExtractor`` feeding it with the\ndeclared ``Response`` dependency.\n\nIn definitive, using ``andi`` in such a framework\ncan provide great flexibility to the user\nand reduce boilerplate.\n\nWeb server example\n******************\n\n``andi`` can be useful also for implementing a new\nweb framework.\n\nLet's imagine a framework where you can declare your sever in a\nclass like the following:\n\n.. code-block:: python\n\n    class MyWeb(Server):\n\n        @route(\"/products\")\n        def productspage(self, request: Request):\n            ... # return the composed page\n\n        @route(\"/sales\")\n        def salespage(self, request: Request):\n            ... # return the composed page\n\nThe former case is composed of two endpoints, one for serving\na page with a summary of sales, and a second one to serve\nthe products list.\n\nConnection to the database can be required\nto sever these pages. This logic could be encapsulated\nin some classes:\n\n.. code-block:: python\n\n    @dataclass\n    class Products:\n        conn: DBConnection\n\n        def get_products()\n            return self.conn.query(\"SELECT ...\")\n\n    @dataclass\n    class Sales:\n        conn: DBConnection\n\n        def get_sales()\n            return self.conn.query(\"SELECT ...\")\n\nNow ``productspage`` and ``salespage`` methods can just declare\nthat they require these objects:\n\n.. code-block:: python\n\n    class MyWeb(Server):\n\n        @route(\"/products\")\n        def productspage(self, request: Request, products: Products):\n            ... # return the composed page\n\n        @route(\"/sales\")\n        def salespage(self, request: Request, sales: Sales):\n            ... # return the composed page\n\nAnd the framework can then be responsible to fulfill these\ndependencies. The flexibility offered would be a great advantage.\nAs an example, if would be very easy to create a page that requires\nboth sales and products:\n\n.. code-block:: python\n\n        @route(\"/overview\")\n        def productspage(self, request: Request,\n                         products: Products, sales: Sales):\n            ... # return the composed overview page\n\n\nContributing\n============\n\n* Source code: https://github.com/scrapinghub/andi\n* Issue tracker: https://github.com/scrapinghub/andi/issues\n\nUse tox_ to run tests with different Python versions::\n\n    tox\n\nThe command above also runs type checks; we use mypy.\n\n.. _tox: https://tox.readthedocs.io\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fscrapinghub%2Fandi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fscrapinghub%2Fandi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fscrapinghub%2Fandi/lists"}