Skip to content

Commit

Permalink
Merge pull request #241 from sanger/DPL258-new-barcode-format
Browse files Browse the repository at this point in the history
DPL-258 new barcode format
  • Loading branch information
emrojo authored May 23, 2022
2 parents 416126c + 18678f9 commit acc82e3
Show file tree
Hide file tree
Showing 27 changed files with 1,511 additions and 156 deletions.
110 changes: 110 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@

Barcode generation using postgres sequences and pre-defined prefixes.

## Features

Baracoda is a JSON-based microservice written in Python and backed in a
PostgreSQL database, with the purpose of handling the creation
of new barcodes for the LIMS application supported currently in PSD.

These are some of the key features currently supported:

* Creation of single barcodes
* Creation of group of barcodes
* Support for children barcodes creation
* Retrieval of the last barcode created for a prefix
* Support for different barcode formats

## Table of Contents

<!-- toc -->
Expand All @@ -24,6 +38,8 @@ Barcode generation using postgres sequences and pre-defined prefixes.
- [Autogenerating Migrations](#autogenerating-migrations)
- [Routes](#routes)
- [Miscellaneous](#miscellaneous)
* [Configuration](#configuration)
* [Children barcode creation](#children-barcode-creation)
* [Troubleshooting](#troubleshooting)
+ [Installing psycopg2](#installing-psycopg2)
* [Updating the Table of Contents](#updating-the-table-of-contents)
Expand Down Expand Up @@ -151,11 +167,105 @@ The following routes are available from this service:
barcode_creation.get_last_barcode GET /barcodes/<prefix>/last
barcode_creation.get_new_barcode POST /barcodes/<prefix>/new
barcode_creation.get_new_barcode_group POST /barcodes_group/<prefix>/new
child_barcodes.new_child_barcodes POST /child-barcodes/<prefix>/new
health_check GET /health
static GET /static/<path:filename>

## Miscellaneous

### Configuration

The default configuration of the currently supported prefixes is specified in the
```baracoda/config/defaults.py``` module. For example:
```json
{
"prefix": "HT",
"sequence_name": "ht",
"formatter_class": GenericBarcodeFormatter,
"enableChildrenCreation": False,
}
```
These are the allowed keywords that we can specify to configure a prefix:

- ```prefix```: This is the string that represents the prefix we are configuring for
supporting new barcodes.
- ```sequence_name```: This is the sequence name in the PostgreSQL database which will
keep record of the last index created for a barcode. Prefixes can share the same
sequence.
- ```formatter_class```: Defines the class that will generate the string that represents
a new barcode by using the prefix and the new value obtained from the sequence.
If we want to support a new formatter class we have to provide a class that implements
the interface ```baracoda.formats.FormatterInterface```.
- ```enableChildrenCreation```: Defines if the prefix has enabled the children creation.
If true, the prefix will support creating barcodes based on a parent barcode
If it is False, the prefix will reject any children creation request for that prefix.

### Children barcode creation

Children barcodes from a parent barcode can be created with a POST request to the
endpoint with a JSON body:

```/child-barcodes/<PREFIX/new```

The inputs for this request will be:

- *Prefix* : prefix where we want to create the children under. This argument will be
extracted from the URL ```/child-barcodes/<PREFIX>/new```
All barcodes for the children will have this prefix (example, prefix HT will generate children
like HT-11111-1, HT-11111-2, etc)
- *Parent Barcode* : barcode that will act as parent of the children. This argument will be
extracted from the Body of the request, eg: ```{'barcode': 'HT-1-1', 'count': 2}```.
To be considered valid, the barcode needs to follow the format ```<PREFIX>-<NUMBER>(-<NUMBER>)?```
where the last number part is optional (it represents if the barcode was a child).
For example, valid barcodes would be ```HT-11111-13``` (normal parent) and ```HT-11112-24```
(parent that was a child) but not ```HT-1-1-1``` or ```HT12341-1```.
- *Child* : part of the Parent barcode string that would identify if the parent was
a child before (the last number). For example for the *Parent barcode* ```HT-11111-14```,
*Child* would be 14; but for the *Parent barcode* ```HT-11111```, *Child* would have no
value defined.
- *Count* : number of children barcodes to create.

#### Wrong parent barcodes

A request with parent barcode that does not follow the format defined like:
```
<PREFIX>-<NUMBER>(-<NUMBER>)?
```
will create normal barcodes instead of suffixed children barcodes (normal barcode creation).

A request that follows the right format but does not comply with current database will be
rejected as impostor barcode. For example, if we receive the parent barcode ```HT-1111-14```, but in the
database the parent ```HT-1111``` has only created 12 child barcodes yet, so ```HT-1111-14``` is
impossible to have been generated by Baracoda.

#### Logic Workflow

The following diagram describes the workflow of how this endpoint will behave depending on
the inputs declared before:


```mermaid
graph TD;
Prefix(Prefix is extracted from URL) --> PrefixEnabled[[Is 'Prefix' enabled by config to create children]];
ParentBarcode(Parent barcode is extracted from URL arg) --> PrefixEnabled[[Is 'Prefix' enabled by config to create children]];
Child(Child is extracted from Parent barcode) --> PrefixEnabled[[Is 'Prefix' enabled by config to create children]];
PrefixEnabled -->|Yes|ValidBarcode[[Is 'Parent Barcode' a valid parent?]];
PrefixEnabled -->|No|Rejected([HTTP 422 - Rejected]);
ValidBarcode -->|Yes|ParentPresent[[Is 'Parent barcode' present in database?]];
ValidBarcode -->|No|NormalBarcode(Generate normal barcodes for the Prefix);
NormalBarcode -->NormalAccept([HTTP 201 - Created])
ParentPresent -->|Yes|ChildExist[[Do we have a value for 'Child'?]];
ParentPresent -->|No|ChildExist2[[Do we have a value for 'Child'?]];
ChildExist -->|Yes|ChildConstraint[[Is 'Child' bigger than the last child generated by the 'Parent barcode' in previous requests?]];
ChildConstraint -->|Yes|Impostor([HTTP 500 - Parent barcode is an impostor]);
ChildExist2 -->|Yes|Impostor;
ChildExist -->|No|ChildrenBarcodes(Generate children barcodes);
ChildrenBarcodes --> NormalAccept;
ChildConstraint -->|No|ChildrenBarcodes
ChildExist2 -->|No|ChildrenBarcodes;
```

### Troubleshooting

#### Installing psycopg2
Expand Down
23 changes: 23 additions & 0 deletions alembic/versions/e501839465f6_add_sqp_sequence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Add sqp sequence
Revision ID: e501839465f6
Revises: bc442d63d7d3
Create Date: 2022-04-04 14:38:35.746245
"""
from alembic import op


# revision identifiers, used by Alembic.
revision = "e501839465f6"
down_revision = "bc442d63d7d3"
branch_labels = None
depends_on = None


def upgrade():
op.execute("CREATE SEQUENCE sqp START 1;")


def downgrade():
op.execute("DROP SEQUENCE IF EXISTS sqp;")
33 changes: 33 additions & 0 deletions alembic/versions/e505f2e15499_add_child_barcode_counter_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""add child barcode counter table
Revision ID: e505f2e15499
Revises: e501839465f6
Create Date: 2022-04-06 14:58:18.990940
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "e505f2e15499"
down_revision = "e501839465f6"
branch_labels = None
depends_on = None


def upgrade():
op.create_table(
"child_barcode_counter",
# ID
sa.Column("barcode", sa.String(50), nullable=False),
sa.Column("child_count", sa.Integer(), nullable=True),
# Created_at
sa.PrimaryKeyConstraint("barcode"),
)


def downgrade():
op.drop_table("child_barcode_counter")
# ### end Alembic commands ###
2 changes: 2 additions & 0 deletions baracoda/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from flask.cli import with_appcontext

from baracoda import barcodes
from baracoda import child_barcodes
from baracoda.config.logging import LOGGING
from baracoda.db import db, reset_db

Expand Down Expand Up @@ -41,6 +42,7 @@ def create_app(test_config=None):
app.cli.add_command(init_db_command)

app.register_blueprint(barcodes.bp)
app.register_blueprint(child_barcodes.bp)

@app.route("/health")
def health_check():
Expand Down
56 changes: 56 additions & 0 deletions baracoda/barcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,26 @@

@bp.post("/barcodes_group/<prefix>/new") # type: ignore
def get_new_barcode_group(prefix: str) -> Tuple[Any, int]:
"""Endpoint that creates a new group of barcodes that are related in one request
Arguments:
- prefix : str - URL extracted argument, that defines the Prefix to use for
the barcodes generated. It has to be one of the prefixes defined in
baracoda.config PREFIXES variable
- count : str - URL or BODY extracted argument. It represents the number of
barcodes we want to create inside the group.
If specified in URL it can be defined as url parameter:
Eg: /barcodes_group/TEST/new?count=14
If specified in BODY it has to be defined as jSON:
Eg: { "count": 14 }
Result:
- Success: HTTP 201 with JSON representation of BarcodeGroup instance
- InvalidPrefixError: HTTP 400 with JSON representation of error.
- InvalidCountError: HTTP 422 with JSON representation of error.
- OtherError: HTTP 500 with JSON representation of error.
"""
try:
logger.debug(f"Creating a barcode group for '{ prefix }'")
count = get_count_param()

operator = BarcodeOperations(prefix=prefix)
Expand All @@ -36,7 +55,19 @@ def get_new_barcode_group(prefix: str) -> Tuple[Any, int]:

@bp.post("/barcodes/<prefix>/new") # type: ignore
def get_new_barcode(prefix: str) -> Tuple[Any, int]:
"""Endpoint that creates one single barcode for a prefix
Arguments:
- prefix : str - URL extracted argument, that defines the Prefix to use for
the barcode generated. It has to be one of the prefixes defined in
baracoda.config PREFIXES variable
Result:
- Success: HTTP 201 with JSON representation of Barcode instance
- InvalidPrefixError: HTTP 400 with JSON representation of error.
- OtherError: HTTP 500 with JSON representation of error.
"""
try:
logger.debug(f"Creating a barcode for '{ prefix }'")
operator = BarcodeOperations(prefix=prefix)
barcode = operator.create_barcode()

Expand All @@ -50,7 +81,20 @@ def get_new_barcode(prefix: str) -> Tuple[Any, int]:

@bp.get("/barcodes/<prefix>/last") # type: ignore
def get_last_barcode(prefix: str) -> Tuple[Any, int]:
"""Endpoint that returns the last generated barcode for a specific prefix
Arguments:
- prefix : str - URL extracted argument, that defines the Prefix we want
to queryu. It has to be one of the prefixes defined in
baracoda.config PREFIXES variable
Result:
- Success: HTTP 200 with JSON representation of barcode instance
- NotFound: HTTP 404 with empty body
- InvalidPrefixError: HTTP 400 with JSON representation of error.
- OtherError: HTTP 500 with JSON representation of error.
"""
try:
logger.debug(f"Obtaining last from '{ prefix }'")
operator = BarcodeOperations(prefix=prefix)

barcode = operator.get_last_barcode(prefix)
Expand All @@ -65,6 +109,18 @@ def get_last_barcode(prefix: str) -> Tuple[Any, int]:


def get_count_param():
"""Extracts the count argument from the HTTP request received.
If specified in URL it can be defined as url parameter:
Eg: /barcodes_group/TEST/new?count=14
If specified in BODY it has to be defined as jSON:
Eg: { "count": 14 }
Arguments: No
Returns one of this:
int - value of the 'count' argument extracted
InvalidCountError - Exception raised when argument could not be extracted
"""
if "count" in request.values:
return int(request.values["count"])
else:
Expand Down
Loading

0 comments on commit acc82e3

Please sign in to comment.