{"id":18828574,"url":"https://github.com/boichee/fabricator","last_synced_at":"2025-04-14T03:13:37.740Z","repository":{"id":52313027,"uuid":"140230065","full_name":"boichee/fabricator","owner":"boichee","description":"Declarative API client creator. Write API clients with just 1 line of code per endpoint.","archived":false,"fork":false,"pushed_at":"2022-12-26T20:56:50.000Z","size":61,"stargazers_count":11,"open_issues_count":6,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-14T03:13:31.025Z","etag":null,"topics":["client","client-library","python2","python3","rest-api","rest-client","sdk","sdk-python"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/boichee.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-07-09T04:11:07.000Z","updated_at":"2019-07-26T01:27:51.000Z","dependencies_parsed_at":"2023-01-31T01:30:30.796Z","dependency_job_id":null,"html_url":"https://github.com/boichee/fabricator","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boichee%2Ffabricator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boichee%2Ffabricator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boichee%2Ffabricator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boichee%2Ffabricator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/boichee","download_url":"https://codeload.github.com/boichee/fabricator/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248813802,"owners_count":21165634,"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":["client","client-library","python2","python3","rest-api","rest-client","sdk","sdk-python"],"created_at":"2024-11-08T01:32:12.319Z","updated_at":"2025-04-14T03:13:37.714Z","avatar_url":"https://github.com/boichee.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"## Fabricator\n\nHave an API? Make a client.\n\n### What is it?\n\n`fabricator` provides a fast, declarative-ish interface for creating clients for APIs. Create clients for ReST APIs in just a few lines of code.\n\n### I don't believe you, show me...\n\nOk, fine. I'll show you.\n\nFirst, you'll need to install fabricator. It's been tested to be compatible with Python 2.7 and 3.6.\n\n#### First, install `fabricator`\n\n**Install with `pip` (Recommended)**\n\n`pip install fabricate-it`\n\n**or, just clone it into your project**\n\n`git submodule add http://github.com/boichee/fabricator.git`\n\n#### Now, use `fabricator`\n\nIn this example, we'll create a client that works with an imaginary \"Todo\" API (I know, boring example...)\nImagine we have a \"Todo API\" (I know, boring example) that looks like this:\n\n```\nGET     /__health\nGET     /api/v1/todos/\nGET     /api/v1/todos/:id\nPOST    /api/v1/todos/\nPUT     /api/v1/todos/:id\nDELETE  /api/v1/todos/:id\n```\n\nYou can create a client for all of these endpoints like this:\n\n```python\nfrom fabricator import Fabricator\n\ndef MyTodoAPI():\n    # Establish a client instance using the Fabricator class\n    client = Fabricator(base_url='https://todos.com')\n    \n    # Now, you start adding your endpoints\n    client.get(name='health', path='/__health')\n    \n    # Endpoints for the To-Do resource\n    # Note: You don't have to create a group, but its a nice feature that saves some typing and \n    # allows you to group handlers and other features (more on that later)\n    todos = client.group(name='todos', prefix='/api/v1/todos') \n    \n    # Now that we have a group, we can create endpoints within it\n    todos.get(name='all', path='/')\n    todos.get(name='one', path='/:id')\n    todos.post(name='create', path='/')\n    todos.put(name='update', path='/:id')\n    todos.delete(name='remove', path='/:id')\n    \n    # .start() locks the Client and prepares it for use.\n    client.start()\n    \n    # And return it, of course\n    return client\n```\n\nActually, since this CRUD structure is so common in ReSTful APIs, there's a shortcut\nmethod to create APIs that have this topology - `.standard()`:\n\n```python\nfrom fabricator import Fabricator\n\ndef MyTodoAPI():\n    # Establish a client instance using the Fabricator class\n    client = Fabricator(base_url='https://todos.com')\n    \n    # Now, you start adding your endpoints\n    client.get(name='health', path='/__health')\n    \n    # Create the group\n    todos = client.group(name='todos', prefix='/api/v1/todos')\n\n    # Now create all the endpoints in one go\n    # Note when using this shortcut the endpoints will have the names:\n    # all, get, create, overwrite, update, delete\n    todos.standard(with_param='id')\n    \n    # .start() locks the Client and prepares it for use.\n    client.start()\n    \n    # And return it, of course\n    return client\n```\n\nOk, that's great. But how do I use that? Glad you asked...\n\n```python\nfrom fabricator.exc import *\nclient = MyTodoAPI()\n\n# Let's try doing a health check with our new API\nresp = client.health() # The `resp` object is a standard requests.Response instance\nprint(\"Status code was: %s\" % resp.status_code)\n\n# Ok, now something more complicated, let's create 5 todos\nfor i in range(5):\n    s = 'Thing to do #{}'.format(i)\n    resp = client.todos.create(value=s)\n    if resp.status_code is not 201:\n        print('The todo was not created!')\n        exit(1)\n    else:\n        print('Successfully created Todo #{}'.format(i))\n    \n# Ok, but how do I find one of the todos?\n# Note that the param 'id' is the same as the ':id' we used above\nresp = client.todos.one(id=1)\nif resp.status_code is not 200:\n    print('Could not get the Todo!')\n    exit(1)\n    \n# Extract the data\nfirst_todo = resp.json()\n\n# first_todo is now a dict with the form { 'id': 1, 'value': 'A thing to do' }\nfirst_todo['value'] = 'Go outside and see the sun!'\n\n# Let's update the todo\nresp = client.todos.update(**first_todo)\nif resp.status_code is not 200:\n    print('The todo with ID %s did not update as expected' % first_todo['id'])\n    exit(1)\n    \n# Actually, who needs it! I can remember to go outside on my own!\n_, status_code = client.todos.remove(id=1)\nif status_code is not 204:\n    print('The Todo with ID 1 was not removed. Oh no!')\n\n```\n\nWow, right?\n\n\n### Response Handlers\n\nYou may not want callers to have access to, or to work directly with, the `requests.Response` object when a call is made. Maybe you want to do some response handling?\n\n#### Use a response handler\n\nA response handler is just a function with the signature `Callable[[request.Response], Any]`\n\nFabricator provides some default response handlers for you to use:\n\n- `handler_json_decode`\n- `handler_check_ok`\n\n```python\nfrom fabricator import Fabricator, handler_json_decode\nclient = Fabricator(base_url='https://todos.com', handler=handler_json_decode)\n```\n\nThis `handler_json_decode` handler is super simple and is just provided for convenience. It does 3 things:\n\n  1. It checks the result of each request, and makes sure the status code was in the 200 or 300 range. If it's not, an `FabricatorRequestError` or `FabricatorRequestAuthError` is raised (if auth was the problem).\n  2. If the request was successful, it will try to decode the body of the request under the assumption it contains `json` data. If that works, it will parse the JSON into python objects. If it doesn't work, it falls back to returning the raw body as a string. \n  3. As long as no request error occurred, it returns a tuple with the form `(response_body, response_status_code)`.\n\nI mention this because it's likely that your API will have some unique differences or you might want your client to return things in a different form.\n\n#### Writing your own response handler\n\nYou can see an example of a custom response handler that effectively creates DAO's. It's in `examples/examples.py`. Here's the gist:\n\n```python\n# A response handler will receive the `requests.Response` instance that comes back from the HTTP request. It's up to you what to do with it.\n\n# Imagine we want to create a MyTodo class and have all responses auto-converted into an instance\n\nclass MyTodo:\n    def __init__(self, id, value):\n        self.id = id\n        self.value = value\n        \ndef handler_todo_response(resp):\n    if not resp.ok:\n        return None\n        \n    try:\n        data = resp.json()\n        return MyTodo(**data)\n    except:\n        return None\n        \nfrom fabricator import Fabricator\nclient = Fabricator(base_url='https://todos.com', handler=handler_todo_response)\n\n# You can set response handlers at any level. So they can be applied to a group, or just\n# a single endpoint, if desired.\n\ntodos = client.group(name='todos', prefix='/api/v1/todos', handler=handler_todo_response)\n\n# or...\n\ntodos.get(name='one', path='/:id', handler=handler_todo_response)\n\n```\n\nIn the example above, if you only apply the handler to a `group`, but not to the parent API, the parent API will use whatever handler it received on endpoints outside of that group. Same goes for a handler set on a specific endpoint--only that endpoint will use the handler if you didn't set the handler at a higher level.\n\n#### The `no-op` response handler\n\nIf you don't provide a value for `handler` when initializing your API, the default is to use the `no-op` response handler. This literally just returns the `requests.Response` instance that the python `requests` module generates.\n\nIf you don't provide a response handler when initializing a `Fabricator` (or using `client.set_handler()`), you're going to want to do this instead:\n\n```python\nfrom fabricator import Fabricator\nclient = Fabricator(base_url='https://todos.com')\nclient.get('health', path='/__health')\nclient.start()\n\n# Call is the same, but notice we're now only expecting a single value as the response\nresp = client.health()\n\n# If you want the status code, do\nprint(resp.status_code)\n\n# If you want the response text, you can do\nprint(resp.text)\n\n# Response json? Sure:\nprint(resp.json())\n\n# Want to know what else there is? Check out the docs for the `requests` package\n``` \n\n#### Setting a `handler` after instantiation\n\nYou can set a `handler` after you instantiate the client with `set_handler`:\n\n```python\nstatus_code_handler = lambda r: r.status_code\n\nclient = Fabricator(...)\nclient.set_handler(handler=status_code_handler)\n```\n\n\n### What about auth?\n\nMost API's do require that you authenticate yourself somehow. To do so here, you create an `auth_handler`. An Auth Handler has the signature:\n\n`Callable[[requests.Request], requests.Request]`\n\nBasically, your auth handler will receive the `Request` instance that is about to be sent to the API, and you can modify any part of it to make auth work properly. \n\n`fabricator` provides a basic auth handler for `JWT` auth. But it's easy to write your own. Let's imagine, for example, that rather than using the `Bearer` scheme, your API prefixes its tokens with `JWT`:\n\n```python\nimport os\n\ndef jwt_auth_handler(req):\n    req.headers['Authorization'] = 'JWT %s' % os.environ['AUTH_TOKEN']\n    return req\n``` \n\nThat's it. Provide that, and every request will have an `Authorization` header added that looks like this:\n\n`Authorization: JWT AAAAAAAAAABBBBBBBBBB`\n\nYou provide it when initializing your API, or group, or even a specific endpoint.\n\n#### What if I don't want auth in some cases?\n\nIn the opposite case, where you don't want auth to happen on a specific endpoint--or within a particular group--you can just supply the provided `no_auth` auth handler. You do that like this:\n\n```python\nfrom fabricator import Fabricator\nfrom fabricator.extras import no_auth\n\nclient = Fabricator(...)\nclient.post(name='login', path='/api/v1/auth/', auth_handler=no_auth)\n```\n\n#### Set 'auth_handler' after instantiation\n\nJust like with response `handler`s, you can set an auth handler at any time using the `.set_auth_handler` method.\n\n### Headers, anyone?\n\nHeaders can be provided at any level. At the top level `API()` instance creation. At the time you create an `client.group()`, or when registering an endpoint. It basically works the same as auth and handlers. Just provide a dict with the headers you want included:\n\n```python\nfrom fabricator import Fabricator\n\n# Now every request will be set to have a content type of JSON unless you override at a deeper level\nclient = Fabricator(..., headers={ 'content-type': 'application/json' })\n```\n\n#### Can I add a header?\n\nYes, you can add a header at any time, to either the root client, or any group using the `.add_header` method.\n\n```python\nfrom fabricator import Fabricator\n\nclient = Fabricator(...)\nclient.add_header(name='X-CUSTOM_HEADER', value='custom_value')\n\n\n# Or you can add to a group the same way\ng = client.group(name='v1', prefix='/api/v1')\ng.add_header(name='X-CUSTOM-HEADER', name='custom_value')\n```\n\n### Collisions\n\nAs of Fabricator 1.1.0, naming collisions are no longer a problem thanks to some additional magic. You can now name your endpoints whatever you'd like.\n\nAs a result, however, it's become more important that you always use keyword parameters when calling the builder functions. This means:\n\n```python\n\n# BAD. Don't do this.\nclient.get('one', path='/:id')\nclient.add_header('X-IP', '127.0.0.1')\n\n# GOOD. Do this instead\nclient.get(name='one', path='/:id')\nclient.add_header(name='X-IP', value='127.0.0.1')\n\n```\n\nFailing to use keyword arguments when building clients with Fabricator can lead to unexpected behavior—**particularly if you mix and match keyword arguments and positional arguments**.\n\n### Advanced Usage\n\nSuppose you want to register an endpoint that works the same way with both the `PUT` and `PATCH` methods. `Fabricator` has a way to save some time:\n\n```python\nfrom fabricator import Fabricator\n\n# Instantiate as usual\nclient = Fabricator(base_url='https://todos.com')\n\n# Now, rather than using the magic \".put\" or \".patch\" methods, we're going to use \".register\". \n# Fabricator uses this under the hood when you use \".put\" or \".patch\".\nclient.register(name='update', path='/todos/:id', methods=['PUT', 'PATCH'])\n\n# Start the client as usual\nclient.start()\n\n# Now you can use the update method to do a 'PUT' automatically (because 'PUT' was 1st in the list you provided above):\nclient.update(id=1, value='Important thing to remember')\n\n# But what if I want to do a patch?\nclient.update.patch(id=1, value='Important thing to remember')\n\n# What if I don't trust that it's really 'PUT'ing?\n# Then you can do it explicitly!\nclient.update.put(id=1, value='Important thing to remember')\n\n```\n\nIn fact, you can always call the execution methods explicitly if you want. But if you're only assigning 1 HTTP method to an endpoint method, there's no need.\n\n\n### Running Tests\n\nFabricator uses `py.test`. To run the test-suite, do the following:\n\nMake sure all dependencies are installed\n\n```bash\npip install -r requirements.txt\n```\n\nThen run the tests\n\n```bash\npy.test\n```\n\nThat's it.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fboichee%2Ffabricator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fboichee%2Ffabricator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fboichee%2Ffabricator/lists"}