1. Tutorial

This tutorial covers the sorting of a set of parts with the AM-Vision.

First we describe the sample set and perform the one-time configuration of the AM-Vision. Then the parts are uploaded, sorted by next post-processing step, and finally sorted by customer order.

The AM-Vision API is a REST API which accepts and returns only JSON data. The main API endpoints are:

Endpoint

Description

model

Models represent 3D models (e.g. STL files).

part

Representation of a printed, physical part.

part_attribute

Part attributes that can be sorted on, shown in the UI or on labels.

batch

Batches are collections of parts to be sorted.

query

Queries on attributes to sort on.

query_category

Categories of queries displayed as tabs in the UI

webhook

Webhook subscriptions to notify your API of events in the AM-Vision

scan_assignment

Contains details of the assignment of scans to parts by operators

material

Materials describe the appearance of printed 3D models.

1.1. Sample data

In this tutorial, we’ll be using sample STL files that can be downloaded from our github repo. This repository also contains the example code discussed below.

Let’s say we have a small part batch consisting of 14 physical parts:

Thumbnail

STL

Title

Copies

Material

Step 1

Step 2

airplane

airplane.stl

Airplane

1

SLS_PA11

shipping

bus

bus.stl

Bus

5

SLS_PA11

shipping

bracket

bracket.stl

Bracket

1

MJF_PA12

shipping

bracket

bracket.stl

Widget

1

MJF_PA12

dye_black

shipping

coupling

coupling.stl

Coupling

1

SLS_PA11

dye_black

shipping

flange

flange.stl

Flange

1

SLS_PA11

dye_red

shipping

knight

knight.stl

Knight

1

MJF_PA12

dye_red

shipping

vase

vase.stl

Vase

3

MJF_PA12

dye_red

shipping

A couple of things to note:

  • There are two raw printing materials: MJF_PA12 and SLS_PA11.

  • There are two processing steps that change the appearance of the parts: dye_black and dye_red.

  • There are 7 STL files, and 8 parts (bracket.stl exists twice under different names)

  • There are 8 part rows, but 14 physical parts (one part has 5 copies and one has 3)

The following table shows example attribute keys and values for each part in the sample set above:

Part

Next step

Previous step

Technology

Order

Print tray

Target date

Airplane

shipping

Printing

SLS

168

240

2022-07-23

Bus

shipping

Printing

SLS

201

240

2022-03-10

Bracket

shipping

Printing

MJF

201

240

2022-03-10

Widget

dye_black

Printing

MJF

443

240

2022-03-18

Coupling

dye_black

Printing

SLS

302

240

2022-03-18

Flange

dye_red

Printing

SLS

201

240

2022-03-10

Knight

dye_red

Printing

MJF

302

240

2022-03-18

Vase

dye_red

Printing

MJF

168

240

2022-07-23

These attribute key-values form the main metadata of the parts, and are used to search, sort, and group the parts.

1.2. Connecting to the API

Connecting to the API can be done using your HTTP library of choice. Authentication is done using a token, which you should have received from AM-Flow. The token needs to be sent in an Authorization header with each request.

The example code below is in Python 3 (using the Slumber library). In the examples below, please replace the following:

AMV_URL:

the url to the API of your AM-Vision machine (e.g. http://192.168.100.123/api/)

AMV_TOKEN:

your API access token (contact AM-Flow if you don’t have one).

The following example defines a bare-bones API client class using Slumber:

import slumber

class APIClient(slumber.API):
    def __init__(self, url, token):
        super().__init__(url)
        # add auth header to slumber's default api client
        header = "Token {}".format(token)
        self._store['session'].auth = None
        self._store['session'].headers['Authorization'] = header

# create a client instance
api = APIClient(AMV_URL, AMV_TOKEN)
# retrieve a batch from the API
api.batch.get()

Note that in the examples, the Slumber library translates the API paths to dot-notation. So when it says api.batch.get(), that means a GET request to the /api/batch/ endpoint.

The AM-Vision API tries to return helpful messages in case of bad requests (400 errors). The API client above however does not display these, only a traceback. The somewhat more complex client example below will display nice error messages and time the api responses:

import logging
import time
import requests
import slumber

log = logging.getLogger(__name__)


class APISession(requests.Session):
    """Logging wrapper around requests session"""
    def request(self, method, url, **kwargs):
        start = time.time()
        response = super().request(method, url, **kwargs)
        duration = int(1000 * (time.time() - start))
        log.info("[AM-Vision API] %s %s %s %dms", method, url, response.status_code, duration)
        if 400 <= response.status_code <= 499:
            log.warning("AMvision Error message: %s", response.content)
        return response


class APIClient(slumber.API):
    def __init__(self, url, token):
        # this custom session adds error printing and response time tracking
        super().__init__(url, session=APISession())
        # add authentication header to slumber's default api client
        self._store['session'].auth = None
        self._store['session'].headers['Authorization'] = "Token " + token

1.3. One-time configuration

There are several things that you will have to configure only once in your AM-Vision. You can ask your AM-Flow service agent to configure these for you, or you can do it yourself with the API.

Note

Please refer to the configuration.py file in our github repo for the example code of the one time configuration.

Part attributes

PartAttributes are the main method for encoding information about your parts. You define attributes once, and then each part can contain a value for each attribute.

Queries on the values of the attributes are used to select parts throughout the system. PartAttributes can be used to sort parts by, to create batches from, to group parts by in the UI, to display in the UI or on printed labels, etc.

PartAttributes contain the following fields:

id:

A unique slug id for the attribute

title:

A human-readable attribute title

field:

The field in your attributes map to use (usually the same as id)

datatype:

one of NUMBER, STRING, BOOLEAN, DATETIME

filtering:

Allow output sorting on this attribute

sorting:

Allow ordering parts by this attribute in the UI.

detail:

Show this attribute in the part’s detail screen.

summary:

Show this attribute in the part’s summary card.

order:

The order to display the attribute in (optional)

is_list:

Whether the attribute is a list of values

The attributes of our sample dataset are listed above in the Attributes table. If you upload the parts with these values, you can then for instance make a batch with the query next_step=shipping.

The following example uploads the PartAttributes mentioned in the table above:

attributes = [
    {
        "id": "next_step",
        "title": "Next step",
        "field": "next_step",
        "datatype": "STRING",
        "filtering": True,
        "sorting": True,
        "detail": True,
        "summary": True,
        "order": 0
    },
    {
        "id": "prev_step",
        "title": "Previous step",
        "field": "prev_step",
        "datatype": "STRING",
        "filtering": True,
        "sorting": True,
        "detail": True,
        "summary": False,
        "order": 1
    },
    {
        "id": "technology",
        "title": "Technology",
        "field": "technology",
        "datatype": "STRING",
        "filtering": True,
        "sorting": False,
        "detail": True,
        "summary": False
    },
    {
        "id": "order",
        "title": "Order #",
        "field": "order_id",
        "datatype": "NUMBER",
        "filtering": True,
        "sorting": False,
        "detail": True,
        "summary": True
    },
    {
        "id": "tray",
        "title": "Print tray",
        "field": "tray",
        "datatype": "STRING",
        "filtering": True,
        "sorting": False,
        "detail": True,
        "summary": False
    },
    {
        "id": "target_date",
        "title": "Target date",
        "field": "target_date",
        "datatype": "STRING",
        "filtering": True,
        "sorting": True,
        "detail": True,
        "summary": True
    }
]
api.part_attribute.put(attributes)

Sorting query categories

Sorting query categories group sorting queries together in tabs in the sorting setup screen of the UI. Sorting query categories contain the following fields:

id:

A unique slug id for the query category

title:

The title displayed in the UI tab

We’ll make categories for next-step-sorting, order-based sorting, and date-based sorting:

categories = [
    {"id": "next_step", "title": "Next step"},
    {"id": "order", "title": "Order #"},
    {"id": "date", "title": "Target date"},
]
api.query_category.put(categories)

Sorting queries

Sorting queries define the criteria used to sort parts into outputs. An example query could be next_step=dye_black. This query would match all parts whose next processing step is dying black. In the AM-Vision UI, this query can be assigned to an output, such that all to-be-black prints will end up together at this output.

You can also define dynamic sorting queries. In cases where the possible values for a PartAttribute are not known in advance. For instance one might want to sorder by order id, but it is not possible to make queries for each possible order id. In this case you can make a dynamic query for the order attribute, and it will automatically create subqueries for each order in the current batch.

Sorting queries contain the following fields:

id:

A unique slug id for the query

title:

The query title displayed in the UI

category:

QueryCategory tab to display query under (optional)

query:

A query on part attributes that determines the parts (leave empty to select all)

dynamic_attribute:

An optional attribute to define a dynamic query

This example creates only dynamic queries to sort by next step, order number or target date:

sorting_queries = [
    {
        "id": "next_step",
        "title": "Next step",
        "query": "",
        "category": "next_step",
        "dynamic_attribute": "next_step"
    },
    {
        "id": "target_date",
        "title": "Target date",
        "query": "",
        "category": "date",
        "dynamic_attribute": "target_date"
    },
    {
        "id": "order",
        "title": "Order #",
        "query": "",
        "category": "order",
        "dynamic_attribute": "order"
    }
]
api.query.put(sorting_queries)

Webhook subscription

The AM-Vision API can let you know when certain events occur by doing a POST to an endpoint in your own API. This is fully described in Webhooks.

Here, we will subscribe to the batch.advance webhook to be notified when the operator has completed sorting a batch:

api.webhook.post({
    'event': 'batch.advance',
    'target': 'http://yourapi.com/on_batch_sorted/'
})

Later we’ll discuss the contents of the webhook notification.

This concludes the one-time configuration for the AM-Vision API.

1.4. Sorting to next step

The remainder of this tutorial concerns steps that need to be repeated for each new set of parts. We’ll first upload the STL files as Models, then define the Part objects and group them in a Batch. This will allow operators to perform the first sorting round (sorting on next processing step) on the AM-Vision.

Note

Please refer to the sorting_to_next_step.py file in our github repo for the example code for this step.

Upload STL files

First we will upload the STL files for our sample set to the model endpoint. This endpoint takes as input an STL file and a unique ID for it. The model/search endpoint is used to quickly check which models have been uploaded already, to prevent re-uploads.

Warning

It’s crucial that two Models that share an id are guaranteed to be identical. If not, this can mean the wrong Part is displayed and sorted.

Models contain the following fields:

id:

A unique slug id for the model

stl:

the STL file

unit:

the unit of the STL file (cm, mm, or inch are supported, default is mm)

This example will upload the STL files (and preventing duplicate uploads):

# map ids to filenames
stls = {
    'airplane': 'stls/airplane.stl',
    'bus': 'stls/bus.stl',
    'flange': 'stls/flange.stl',
    'vase': 'stls/vase.stl',
    'bracket': 'stls/bracket.stl',
    'coupling': 'stls/coupling.stl',
    'knight': 'stls/knight.stl'
}
# [optional] filter out previously uploaded models
ids = {'id': ','.join(stls.keys())}
response = api.model.search.post(ids, page_size=len(stls))
for reference in response['results']:
    del stls[reference['id']]
# upload models
for id, filename in stls.items():
    with open(filename, 'rb') as stl:
        api.model.post({'id': id, 'unit': unit}, files={'stl': stl})

Define parts

Parts are the principal objects in the AM-Vision API and represent physical parts. The Part object contains the following fields:

id:

A unique id for the part

title:

The part title displayed in the UI

copies:

The number of duplicate copies of this part

model:

Your id of the previously uploaded STL file

material:

An identifier for the part’s appearance

attributes:

A key-value map containing any further metadata

The following example uploads the parts as defined in the sample dataset (See the Parts table and the Attributes table):

parts = [
    {
        "id": "airplane",
        "title": "Airplane",
        "copies": 1,
        "model": "airplane",
        "material": "SLS_PA11",
        "attributes": {
            "prev_step": "printing",
            "next_step": "shipping",
            "technology": "SLS",
            "order_id": 168,
            "tray": "240",
            "target_date": "2022-07-23"
        }
    },
    {
        "id": "bus",
        "title": "Bus",
        "copies": 5,
        "model": "bus",
        "material": "SLS_PA11",
        "attributes": {
            "prev_step": "Printing",
            "next_step": "shipping",
            "technology": "SLS",
            "order_id": 201,
            "tray": "240",
            "target_date": "2022-03-10"
        }
    },
    {
        "id": "bracket",
        "title": "Bracket",
        "copies": 1,
        "model": "bracket",
        "material": "MJF_PA12",
        "attributes": {
            "prev_step": "Printing",
            "next_step": "shipping",
            "technology": "MJF",
            "order_id": 201,
            "tray": "240",
            "target_date": "2022-03-10"
        }
    },
    {
        "id": "widget",
        "title": "Widget",
        "copies": 1,
        "model": "bracket",
        "material": "MJF_PA12",
        "attributes": {
            "prev_step": "Printing",
            "next_step": "dye_black",
            "technology": "MJF",
            "order_id": 443,
            "tray": "240",
            "target_date": "2022-03-18"
        }
    },
    {
        "id": "coupling",
        "title": "Coupling",
        "copies": 1,
        "model": "coupling",
        "material": "SLS_PA11",
        "attributes": {
            "prev_step": "Printing",
            "next_step": "dye_black",
            "technology": "SLS",
            "order_id": 302,
            "tray": "240",
            "target_date": "2022-03-18"
        }
    },
    {
        "id": "flange",
        "title": "Flange",
        "copies": 1,
        "model": "flange",
        "material": "SLS_PA11",
        "attributes": {
            "prev_step": "Printing",
            "next_step": "dye_red",
            "technology": "SLS",
            "order_id": 201,
            "tray": "240",
            "target_date": "2022-03-10"
        }
    },
    {
        "id": "knight",
        "title": "Knight",
        "copies": 1,
        "model": "knight",
        "material": "MJF_PA12",
        "attributes": {
            "prev_step": "Printing",
            "next_step": "dye_red",
            "technology": "MJF",
            "order_id": 302,
            "tray": "240",
            "target_date": "2022-03-18"
        }
    },
    {
        "id": "vase",
        "title": "Vase",
        "copies": 3,
        "model": "vase",
        "material": "MJF_PA12",
        "attributes": {
            "prev_step": "Printing",
            "next_step": "dye_red",
            "technology": "MJF",
            "order_id": 168,
            "tray": "240",
            "target_date": "2022-07-23"
        }
    }
]
api.part.put(parts)

Define batch

Batches are groups of Part objects, defined by a query on PartAttributes. An operator can start a batch from the main page of the UI to start sorting the Parts inside.

The Batch object contains the following fields:

id:

A unique id for the batch

title:

The batch title displayed in the UI

batch_category:

Optional batch_category tab to display batch under (see BatchCategories)

query:

A query on part attributes that determines the parts in the batch

The parts in our sample set all come from the same print tray 240 and this is how we will group them. The following example defines the batch:

batch = {
    "id": "240",
    "title": "Tray 240",
    "query": "tray=240",
}
api.batch.post(batch)

The system is now ready for the first round of sorting by the operator.

Sorting the parts

After our work above, the first batch is now visible in the AM-Vision UI:

_images/batch.png

The operator can now start sorting the batch. He/she will first be asked to setup the sorting in this screen:

_images/output_assignments.png

The operator sets up sorting by Next step, dragging each next step to an output:

_images/full_output_assignments.png

The operator now feeds each part through the machine. The machine will recognize the part and automatically place it in the requested output. Occasionally the operator will be required to confirm the recognition result:

_images/result.png

After sorting all the parts, the operator can now advance the batch:

_images/batch_finalize.png

When the batch is advanced, the batch.advance webhook that we subscribed to earlier is sent.

1.5. Sorting to order

After the batch is advanced, the parts need to be updated and new batches created for the second round of sorting.

Note

Please refer to the sorting_to_order.py file in our github repo for the example code for the sorting to order step. That example does not include the webhook listening part, but instead manually checks whether the batch is advanced

Webhook notification

When the operator completes sorting the batch, you will receive the batch.advance webhook. The webhook notification only contains a summary of the batch, like this:

{
    'hook': {
        'id': 18,
        'event': 'batch.advance',
        'target': 'http://yourapi.com/on_batch_sorted/'
    },
    'data': {
        'url': '/api/batch/240/',
        'created': '2022-01-19T12:05:16.655027+01:00',
        'modified': '2022-01-19T12:10:30.787503+01:00',
        'id': '240',
        'title': 'Tray 240',
        'archived': False,
        'batch_category': None,
        'query': 'tray=240',
        'summary': {
            'assigned': 12,
            'reprinted': 2,
            'parts': 14,
            'unassigned_scans': 0,
            'needs_processing': False,
            'processing_status': 'READY',
            'processing_ratio': 1.0,
            'conflicted': 0,
            'currently_scanning': None
        },
        'has_output_assignments': True,
        'active_scan_session': '176719ae-29fb-4eb0-b41f-45f60e23f59d',
        'auto_delete': False
    }
}

We can see 12 parts were assigned, but 2 parts were marked reprints. To get the full details of the assignments we can query the ScanAssignment endpoint.

Retrieving assignments

We can retrieve all the scanassignments for a given batch with a query:

# equivalent to: GET /api/scan_assignment/?batch=999
api.scan_assignment.get(batch=999)

ScanAssignment objects contain the following fields:

uuid:

auto-generated uuid for the assignment

scan:

the uuid of the scan

part:

the id of the part

output:

the id of the output

copies:

the number of part copies that were assigned

reprint:

whether the part was marked as reprint (e.g. rejected)

This could be an example result (I’m leaving out some fields for brevity):

{
    'count': 9,
    'next': None,
    'previous': None,
    'results': [
        {
            'part': 'bus',
            'output': 'left-1',
            'copies': 3,
            'reprint': False
        }
        {
            'part': 'airplane',
            'output': 'right-2',
            'copies': 1,
            'reprint': True
        },
        {
            'part': 'bus',
            'output': 'default',
            'copies': 2,
            'reprint': False
        },
        {
            'part': 'bracket',
            'output': 'right-2',
            'copies': 1,
            'reprint': False
        },
        {
            'part': 'widget',
            'output': 'right-2',
            'copies': 1,
            'reprint': False
        },
        {
            'part': 'coupling',
            'output': 'right-2',
            'copies': 1,
            'reprint': False
        },
        {
            'part': 'flange',
            'output': 'right-2',
            'copies': 1,
            'reprint': False
        },
        {
            'part': 'knight',
            'output': 'right-2',
            'copies': 1,
            'reprint': True
        },
        {
            'part': 'vase',
            'output': 'right-2',
            'copies': 3,
            'reprint': False
        },
    ],
    'num_pages': 1
}

The airplane and knight parts were marked reprints. It is beyond the scope of this tutorial on how to handle reprints. Here I assume they are removed from the second-round sort.

Note that it’s possible for a part with multiple copies to be assigned in separate assignments. In the above result, The bus is assigned 3 copies first, then 2 more later on.

Also note that this result is paginated and will by default only show 10 results. In this case there’s only 9. if ‘num_pages’ is bigger than one you will need to retrieve the remaining pages. The link to the next page is given in the result in case there’s more than one page, like so:

{
    "count": 50,
    "next": "http://localhost:8000/api/scan_assignment/?batch=sample&page=2",
    "previous": null,
    "results": ....,
    "num_pages: 5
}

You can also specify the page_size in the request, e.g.:

api.scan_assignment.get(batch=240, page_size=1000)

If you wish to avoid pagination you could specify the sum of all the part copies as the page_size.

Updating the AM-Vision

First, parts that were marked reprint can be removed from the AM-Vision:

api.part.delete(id="airplane,knight")

The remaining parts should be updated to the next processing step, simply by re-defining them and changing the next processing step:

parts = [
    {
        "id": "widget",
        "title": "Widget",
        "copies": 1,
        "model": "bracket",
        "material": "MJF_PA12-BLACK",
        "attributes": {
            "prev_step": "dye_black",
            "next_step": "shipping",
            "technology": "MJF",
            "order_id": 443,
            "tray": "240",
            "target_date": "2022-03-18"
        }
    },
    {
        "id": "coupling",
        "title": "Coupling",
        "copies": 1,
        "model": "coupling",
        "material": "SLS_PA11-BLACK",
        "attributes": {
            "prev_step": "dye_black",
            "next_step": "shipping",
            "technology": "SLS",
            "order_id": 302,
            "tray": "240",
            "target_date": "2022-03-18"
        }
    },
    {
        "id": "flange",
        "title": "Flange",
        "copies": 1,
        "model": "flange",
        "material": "SLS_PA11-RED",
        "attributes": {
            "prev_step": "dye_red",
            "next_step": "shipping",
            "technology": "SLS",
            "order_id": 201,
            "tray": "240",
            "target_date": "2022-03-10"
        }
    },
    {
        "id": "vase",
        "title": "Vase",
        "copies": 3,
        "model": "vase",
        "material": "MJF_PA12-RED",
        "attributes": {
            "prev_step": "dye_red",
            "next_step": "shipping",
            "technology": "MJF",
            "order_id": 168,
            "tray": "240",
            "target_date": "2022-07-23"
        }
    }
]
api.part.put(parts)

Note that we didn’t update the bus and bracket parts, since their next_step did not change (it was already shipping).

Finally we will make new sub-batches for each group of parts that is kept together from the post-processing operation. This comes down to three sub-batches:

  • 2 Parts that had no post-processing step

  • 2 Parts that were dyed black

  • 2 Parts that were dyed red

This will create those sub-batches:

sub_batches = [
    {
        "id": "240_no_post_process",
        "title": "Tray 240: no post process",
        "query": "tray=240&prev_step=printing",
    },
    {
        "id": "240_dye_black",
        "title": "Tray 240: Dye black",
        "query": "tray=240&prev_step=dye_black",
    },
    {
        "id": "240_dye_red",
        "title": "Tray 240: Dye red",
        "query": "tray=240&prev_step=dye_red",
    }
]
api.batch.put(sub_batches)

We can now also delete the original batch:

api.batch(240).delete()

Now we’re ready to start sorting by order id.

Sorting the parts

After our work above, the sub batches are now visible in the AM-Vision UI:

_images/sub_batches.png

The operator can now start sorting these. He/she will first be asked to setup the sorting in this screen:

_images/output_assignments_second_sorting.png

The operator sets up sorting by Order, dragging each order id to an output:

_images/full_output_assignments_second_sorting.png

The operator now feeds each part through the machine. After sorting all the parts, the operator again advances the batch.

Clean up

When receiving the batch.advance webhook for the sub-batches, we can again retrieve the scan assignments, and remove reprints. It is also advisable to delete the sub-batches as well as the parts to avoid clogging up your AM-Vision.

1.6. AM-Quality

This section covers usage of additional API endpoints and models for AM-Quality.

Inspection reports

Comparing to AM-Vision API, there is a new endpoint inspection_report. The inspection report is the result of the quality analysis.

Each scan in the system can have exactly one or zero inspection reports. The scan is created first (at part entry), and the inspection report is added after the analysis is ready.

An nspection report carries the information of a quality scan, the evaluation metrics, and metrology data (in case a metrology template is present, see Metrology templates to know how to use the api to upload a .xvgt file and link it to a part).

Main fields of the inspection report are:

uuid:

A unique id

metrics:

dict, measurements used to determine the automatic PASS/FAIL verdict

metrology_data:

dict, results for the measurements specified in the MetrologyTemplate

scan:

string, The scan uuid for which this report was produced

passed:

boolean, the automatic PASS/FAIL verdict

approved:

boolean, operator verdict

part:

string, id of the part (NOT uuid)

pdf_url:

string, the url to download the PDF version of this report

Here’s an example to retrieve inspection reports with the Slumber API client defined earlier in this tutorial:

# retrieve the inspection report for a given scan
res = api.inspection_report.get(scan=scan_uuid)
inspection_report = res["results"][0]
# get the PDF binary content from 'render_pdf' endpoint
content = api.inspection_report(inspection_report["uuid"]).render_pdf.get()

Metrology templates

Metrology templates are .xvgt files that you can get from VGStudio Max software. You can upload .xvgt files that specify what to measure on the product by using the api endpoint api/metrology_template/.

The following example uses the Slumber API client to upload a metrology template and updates a part with its id:

template_path = "/path/to/xvgt/file"
api.metrology_template.post({"id": "my_xvg_template", files={"template": open(template_path, "rb")})
# link the metrology template to an existing part in the database
part_id = "batch_01_part_1"
# set the metrology_template attribute with the id
api.part(part_id).patch({"metrology_template": "my_xvg_template"}))

Webhook scan.inspect

The webhook scan.inspect is an additional event that AM-Quality backend sends when an inspection report is ready. You can register and listen to this event for further processing (e.g. align with your systems or export CSV/PDF reports):

api.webhook.post({
    'event': 'scan.inspect',
    'target': 'http://yourapi.com/on_inspection_report/'
})

The target endpoint callback receives a payload with the uuid of the inspection report object (see the Webhooks section for more details). The inspection report can subsequently be fetched from the API. Here’s an example webhook JSON output:

{
    'hook': {
        'id': 18,
        'event': 'scan.inspect',
        'target': 'http://yourapi.com/on_inspection_report/'
    },
    'data': {
        'uuid': 'e3ef74e5-a38f-4c26-8b3a-53f98110aa58',
        'url': '<AM-QUALITY-URL>/api/inspection_report/e3ef74e5-a38f-4c26-8b3a-53f98110aa58/'
    }
}

Below is an example scan.inspect webhook receiver. it listens to the webhook, fetches the part and scan ids, and downloads a pdf version of the report:

# Flask python code for the callback view previously defined in ``target``
from flask import Flask, request

def on_inspection_report():
    post = request.json
    report_uuid = post["data"]["uuid"]
    log.info("An InspectionReport was created with uuid: %s", report_uuid)
    # Get the full inspection report from API
    inspection_report = api.inspection_report(report_uuid).get()

    # scan_uuid and part_id can be retrieved from the report
    scan_uuid = inspection_report['scan']
    part_id = inspection_report['part']

    # a pdf version of the report can be downloaded
    log.info("Downloading pdf from %s", inspection_report["pdf_url"])
    content = api.inspection_report(report_uuid).render_pdf.get()
    with open("./report.pdf", "wb") as f:
        f.write(content)

app = Flask(__name__)
app.add_url_rule(
    rule="/on_inspection_report/",
    endpoint="on_inspection_report",
    view_func=on_inspection_report,
    methods=["POST"],
)
app.run(port=5000, debug=True)