Skip to content

Commit

Permalink
Add initial implementation of /parts endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
ubruhin committed Feb 20, 2024
1 parent 44ab523 commit b72ecbe
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 0 deletions.
25 changes: 25 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
ARG ALPINE_TAG
FROM alpine:$ALPINE_TAG

# Install packages.
RUN apk add --no-cache \
python3 \
py3-flask \
py3-flask-pyc \
py3-gunicorn \
py3-gunicorn-pyc

# Copy files.
COPY app.py app/
COPY static/ app/static/
WORKDIR app

# Set entrypoint.
ENTRYPOINT [ \
"gunicorn", \
"--access-logfile=-", \
"--bind=0.0.0.0:8000", \
"--forwarded-allow-ips=*", \
"--workers=4", \
"app:app" \
]
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# librepcb-api-server

Official server-side implementation of the
[LibrePCB API](https://developers.librepcb.org/d1/dcb/doc_server_api.html)
as accessed by the LibrePCB application. Note that some older API paths are
implemented in a different way and might be migrated to this repository
later.

## Requirements

Only Docker Compose is needed to run this server on a Linux machine.

## Usage

For local development, the server can be run with this command:

```bash
docker-compose up --build
```

Afterwards, the API runs on http://localhost:8000/:

```bash
curl -X POST -H "Content-Type: application/json" -d @demo-request.json \
'http://localhost:8000/api/v1/parts/query' | jq '.'
```

## License

The content in this repository is published under the
Expand Down
128 changes: 128 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-

from random import randrange
from urllib.parse import urlencode

from flask import Flask, make_response, request, send_from_directory, url_for
from werkzeug.middleware.proxy_fix import ProxyFix

app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)

PARTS_MAX_COUNT = 20

STATUS_VALUES = [
"Active",
"Active",
"Active",
"Active",
"Active",
"Active",
"Active",
"Active",
"Active",
"Active",
"NRND",
"Obsolete",
"Preview",
]

AVAILABILITY_VALUES = [
10,
5,
5,
5,
5,
5,
5,
0,
0,
0,
0,
0,
-5,
-10,
]

PICTURE_VALUES = [
"https://www.vishay.com/images/product-images/pt-large/81857-pt-large.jpg", # noqa: E501
"https://www.ti.com/content/dam/ticom/images/products/package/d/d0008a.png", # noqa: E501
"https://mm.digikey.com/Volume0/opasdata/d220001/medias/images/2088/100-UFBGA%287x7%29.jpg", # noqa: E501
"https://www.luminus.com/img/new/Ecosystem_MP-MP-5050-240H.png", # noqa: E501
]

PRICE_VALUES = [
[dict(quantity=1, price=0.008), dict(quantity=100, price=0.007)],
[dict(quantity=1, price=0.01), dict(quantity=100, price=0.008)],
[dict(quantity=1, price=0.12), dict(quantity=100, price=0.11)],
[dict(quantity=1, price=0.125), dict(quantity=100, price=0.11)],
[dict(quantity=1, price=0.125), dict(quantity=100, price=0.11)],
[dict(quantity=1, price=0.125), dict(quantity=100, price=0.11)],
[dict(quantity=1, price=0.125), dict(quantity=100, price=0.11)],
[dict(quantity=1, price=0.125), dict(quantity=100, price=0.11)],
[dict(quantity=1, price=0.125), dict(quantity=100, price=0.11)],
[dict(quantity=1, price=0.5), dict(quantity=100, price=0.45)],
[dict(quantity=1, price=1.0), dict(quantity=100, price=0.98)],
[dict(quantity=1, price=15.2), dict(quantity=100, price=14.8)],
]


def _get_random(values):
return values[randrange(len(values))]


@app.route('/api/v1/parts', methods=['GET'])
def parts():
response = make_response(dict(
provider_name="LibrePCB",
provider_url="https://librepcb.org",
provider_logo_url=url_for('parts_static',
filename='parts-provider-librepcb.png',
_external=True),
query_url=url_for('parts_query', _external=True),
max_parts=PARTS_MAX_COUNT,
))
response.headers['Cache-Control'] = 'max-age=300'
return response


@app.route('/api/v1/parts/static/<filename>', methods=['GET'])
def parts_static(filename):
return send_from_directory(
'static', filename, mimetype='image/png', max_age=24*3600)


@app.route('/api/v1/parts/query', methods=['POST'])
def parts_query():
rx = request.get_json()
tx = dict(parts=[])
for rx_part in rx['parts'][:PARTS_MAX_COUNT]:
part = dict(
mpn=rx_part['mpn'],
manufacturer=rx_part['manufacturer'],
results=min(1, randrange(10)),
)
if randrange(10) < 8:
part['product_url'] = "https://duckduckgo.com/?{}".format(
urlencode(dict(q=rx_part['mpn'])))
if randrange(10) < 8:
part['picture_url'] = _get_random(PICTURE_VALUES)
if randrange(10) < 8:
part['pricing_url'] = "https://partstack.com/s?{}".format(
urlencode(dict(pn=rx_part['mpn'])))
if randrange(10) < 8:
part['status'] = _get_random(STATUS_VALUES)
if randrange(10) < 8:
part['availability'] = _get_random(AVAILABILITY_VALUES)
if randrange(10) < 8:
part['prices'] = _get_random(PRICE_VALUES)
if randrange(10) < 8:
part['resources'] = [
dict(
name="Datasheet",
mediatype="application/pdf",
url="https://www.vishay.com/docs/81857/1n4148.pdf",
),
]
tx['parts'].append(part)
return tx
8 changes: 8 additions & 0 deletions demo-request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"parts": [
{
"mpn": "1N4148",
"manufacturer": "Vishay"
}
]
}
15 changes: 15 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
version: "3.8"

services:
server:
build:
context: .
args:
ALPINE_TAG: '3.19'
ports:
- 8000:8000
environment:
FLASK_RUN_DEBUG: 1
FLASK_RUN_HOST: '0.0.0.0'
FLASK_RUN_PORT: 8000 # Same as Gunicorn
entrypoint: ['flask', 'run']
Binary file added static/parts-provider-librepcb.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit b72ecbe

Please sign in to comment.