Skip to content

Commit 1653508

Browse files
authored
Merge pull request #56 from scaleapi/da/circleci
Fix all tests + first pass at CI
2 parents 19af381 + f699d56 commit 1653508

16 files changed

+205
-121
lines changed

.DS_Store

-6 KB
Binary file not shown.

.circleci/config.yml

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# CircleCI jobs are only enabled to on Pull Requests and commits to master branch.
2+
# "Only build pull requests" enabled in Project's Advanced Settings.
3+
version: 2.1
4+
jobs:
5+
build_test:
6+
docker:
7+
- image: python:3.6-slim-buster
8+
resource_class: small
9+
steps:
10+
- checkout # checkout source code to working directory
11+
- run:
12+
name: Install Environment Dependencies
13+
command: | # install dependencies
14+
pip install --upgrade pip
15+
pip install poetry
16+
poetry install
17+
- run:
18+
name: Black Formatting Check # Only validation, without re-formatting
19+
command: |
20+
poetry run black --check -t py36 .
21+
- run:
22+
name: Flake8 Lint Check # Uses setup.cfg for configuration
23+
command: |
24+
poetry run flake8 . --count --statistics
25+
- run:
26+
name: Pylint Lint Check # Uses .pylintrc for configuration
27+
command: |
28+
poetry run pylint nucleus
29+
- run :
30+
name: MyPy typing check
31+
command: |
32+
poetry run mypy --ignore-missing-imports nucleus
33+
- run:
34+
name: Pytest Test Cases
35+
command: | # Run test suite, uses NUCLEUS_TEST_API_KEY env variable
36+
mkdir test_results
37+
poetry run coverage run --include=nucleus/* -m pytest --junitxml=test_results/junit.xml
38+
poetry run coverage report
39+
poetry run coverage html
40+
41+
- store_test_results:
42+
path: htmlcov
43+
44+
- store_test_results:
45+
path: test_results
46+
47+
- store_artifacts:
48+
path: test_results
49+
pypi_publish:
50+
docker:
51+
- image: cimg/python:3.6
52+
steps:
53+
- checkout # checkout source code to working directory
54+
- run:
55+
name: Validate Tag Version # Check if the tag name matches the package version
56+
command: |
57+
PKG_VERSION=$(sed -n 's/^version = //p' pyproject.toml | sed -e 's/^"//' -e 's/"$//')
58+
if [[ "$CIRCLE_TAG" != "v${PKG_VERSION}" ]]; then
59+
echo "ERROR: Tag name ($CIRCLE_TAG) must match package version (v${PKG_VERSION})."
60+
exit 1;
61+
fi
62+
- run:
63+
name: Validate SDK Version Increment # Check if the version is already on PyPI
64+
command: |
65+
PKG_VERSION=$(sed -n 's/^version = //p' pyproject.toml | sed -e 's/^"//' -e 's/"$//')
66+
if pip install "scale-nucleus>=${PKG_VERSION}" > /dev/null 2>&1;
67+
then
68+
echo "ERROR: You need to increment to a new version before publishing!"
69+
echo "Version (${PKG_VERSION}) already exists on PyPI."
70+
exit 1;
71+
fi
72+
- run:
73+
name: Build
74+
command: | # install env dependencies
75+
poetry build
76+
- run:
77+
name: Publish to PyPI
78+
command: |
79+
if test -z "${PYPI_USERNAME}" || test -z "${PYPI_PASSWORD}" ; then
80+
echo "ERROR: Please assign PYPI_USERNAME and PYPI_PASSWORD as environment variables"
81+
exit 1
82+
fi
83+
poetry publish --username=$PYPI_USERNAME --password=$PYPI_PASSWORD
84+
workflows:
85+
build_test_publish:
86+
jobs:
87+
- build_test:
88+
filters:
89+
tags:
90+
only: /^v\d+\.\d+\.\d+$/ # Runs only for tags with the format [v1.2.3]
91+
- pypi_publish:
92+
requires:
93+
- build_test
94+
filters:
95+
branches:
96+
ignore: /.*/ # Runs for none of the branches
97+
tags:
98+
only: /^v\d+\.\d+\.\d+$/ # Runs only for tags with the format [v1.2.3]

README.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -169,14 +169,9 @@ pre-commit install
169169
```
170170

171171
**Best practices for testing:**
172-
(1). Before running pytest, please make sure to authenticate into AWS, since some of the unit tests rely on AWs resources:
172+
(1). Please run pytest from the root directory of the repo, i.e.
173173
```
174-
gimme_okta_aws_creds
175-
```
176-
177-
(2). Please run pytest from the root directory of the repo, i.e.
178-
```
179-
pytest tests/test_dataset.py
174+
poetry pytest tests/test_dataset.py
180175
```
181176

182177

nucleus/annotation.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from dataclasses import dataclass
12
from enum import Enum
23
from typing import Dict, Optional, Any, Union, List
34
from .constants import (
@@ -21,10 +22,11 @@
2122
ANNOTATIONS_KEY,
2223
)
2324

24-
from dataclasses import dataclass
25-
2625

2726
class Annotation:
27+
reference_id: Optional[str] = None
28+
item_id: Optional[str] = None
29+
2830
def _check_ids(self):
2931
if bool(self.reference_id) == bool(self.item_id):
3032
raise Exception(
@@ -69,9 +71,9 @@ def to_payload(self) -> dict:
6971
class SegmentationAnnotation(Annotation):
7072
mask_url: str
7173
annotations: List[Segment]
74+
annotation_id: Optional[str] = None
7275
reference_id: Optional[str] = None
7376
item_id: Optional[str] = None
74-
annotation_id: Optional[str] = None
7577

7678
def __post_init__(self):
7779
if not self.mask_url:
@@ -111,8 +113,8 @@ class AnnotationTypes(Enum):
111113
POLYGON = POLYGON_TYPE
112114

113115

114-
@dataclass
115-
class BoxAnnotation(Annotation):
116+
@dataclass # pylint: disable=R0902
117+
class BoxAnnotation(Annotation): # pylint: disable=R0902
116118
label: str
117119
x: Union[float, int]
118120
y: Union[float, int]

nucleus/dataset_item.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
from dataclasses import dataclass
12
import os.path
3+
from typing import Optional
24
from .constants import (
35
IMAGE_URL_KEY,
46
METADATA_KEY,
@@ -7,16 +9,14 @@
79
DATASET_ITEM_ID_KEY,
810
)
911

10-
from dataclasses import dataclass
11-
1212

1313
@dataclass
1414
class DatasetItem:
1515

1616
image_location: str
17-
reference_id: str = None
18-
item_id: str = None
19-
metadata: dict = None
17+
reference_id: Optional[str] = None
18+
item_id: Optional[str] = None
19+
metadata: Optional[dict] = None
2020

2121
def __post_init__(self):
2222
self.image_url = self.image_location

nucleus/prediction.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from __future__ import annotations
2-
31
from typing import Dict, Optional, List, Any
42
from .annotation import (
53
BoxAnnotation,
@@ -30,7 +28,7 @@ class SegmentationPrediction(SegmentationAnnotation):
3028
# No need to define init or to_payload methods because
3129
# we default to functions defined in the parent class
3230
@classmethod
33-
def from_json(cls, payload: dict) -> SegmentationPrediction:
31+
def from_json(cls, payload: dict):
3432
return cls(
3533
mask_url=payload[MASK_URL_KEY],
3634
annotations=[
@@ -58,15 +56,15 @@ def __init__(
5856
metadata: Optional[Dict] = None,
5957
):
6058
super().__init__(
61-
label,
62-
x,
63-
y,
64-
width,
65-
height,
66-
reference_id,
67-
item_id,
68-
annotation_id,
69-
metadata,
59+
label=label,
60+
x=x,
61+
y=y,
62+
width=width,
63+
height=height,
64+
reference_id=reference_id,
65+
item_id=item_id,
66+
annotation_id=annotation_id,
67+
metadata=metadata,
7068
)
7169
self.confidence = confidence
7270

@@ -78,7 +76,7 @@ def to_payload(self) -> dict:
7876
return payload
7977

8078
@classmethod
81-
def from_json(cls, payload: dict) -> BoxPrediction:
79+
def from_json(cls, payload: dict):
8280
geometry = payload.get(GEOMETRY_KEY, {})
8381
return cls(
8482
label=payload.get(LABEL_KEY, 0),
@@ -106,7 +104,12 @@ def __init__(
106104
metadata: Optional[Dict] = None,
107105
):
108106
super().__init__(
109-
label, vertices, reference_id, item_id, annotation_id, metadata
107+
label=label,
108+
vertices=vertices,
109+
reference_id=reference_id,
110+
item_id=item_id,
111+
annotation_id=annotation_id,
112+
metadata=metadata,
110113
)
111114
self.confidence = confidence
112115

@@ -118,7 +121,7 @@ def to_payload(self) -> dict:
118121
return payload
119122

120123
@classmethod
121-
def from_json(cls, payload: dict) -> PolygonPrediction:
124+
def from_json(cls, payload: dict):
122125
geometry = payload.get(GEOMETRY_KEY, {})
123126
return cls(
124127
label=payload.get(LABEL_KEY, 0),

nucleus/slice.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class Slice:
99
def __init__(self, slice_id: str, client):
1010
self.slice_id = slice_id
1111
self._client = client
12-
12+
1313
def __repr__(self):
1414
return f"Slice(slice_id='{self.slice_id}', client={self._client})"
1515

pyproject.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,19 @@ python = "^3.6"
3636
grequests = "^0.6.0"
3737
requests = "^2.25.1"
3838
tqdm = "^4.60.0"
39+
boto3 = "^1.17.53"
40+
mypy = "^0.812"
41+
coverage = "^5.5"
42+
dataclasses = { version = "^0.7", python = "^3.6.1, <3.7" }
3943

4044
[tool.poetry.dev-dependencies]
4145
poetry = "^1.1.5"
4246
pytest = "^6.2.3"
4347
pylint = "^2.7.4"
44-
boto3 = "^1.17.51"
48+
black = "^20.8b1"
49+
flake8 = "^3.9.1"
50+
mypy = "^0.812"
51+
coverage = "^5.5"
4552

4653

4754
[build-system]

tests/helpers.py

Lines changed: 16 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,25 @@
11
from pathlib import Path
2+
import time
23
from urllib.parse import urlparse
3-
import boto3
4+
45
from nucleus import DatasetItem, BoxPrediction
56

67
PRESIGN_EXPIRY_SECONDS = 60 * 60 * 24 * 2 # 2 days
78

89
TEST_MODEL_NAME = "[PyTest] Test Model"
9-
TEST_MODEL_REFERENCE = "[PyTest] Test Model Reference"
1010
TEST_MODEL_RUN = "[PyTest] Test Model Run"
1111
TEST_DATASET_NAME = "[PyTest] Test Dataset"
1212
TEST_SLICE_NAME = "[PyTest] Test Slice"
1313

14-
TEST_MODEL_NAME = "[PyTest] Test Model Name"
15-
TEST_MODEL_REFERENCE = "[PyTest] Test Model Reference"
16-
TEST_MODEL_RUN = "[PyTest] Test Model Run Reference"
17-
TEST_DATASET_NAME = "[PyTest] Test Dataset"
18-
TEST_SLICE_NAME = "[PyTest] Test Slice"
14+
1915
TEST_IMG_URLS = [
20-
"s3://scaleapi-cust-lidar/Hesai/raw_data/2019-5-11/hesai_data_1557540003/undistorted/front_camera/1557540143.650423lf.jpg",
21-
"s3://scaleapi-cust-lidar/Hesai/raw_data/2019-5-11/hesai_data_1557540003/undistorted/back_camera/1557540143.600352lf.jpg",
22-
"s3://scaleapi-cust-lidar/Hesai/raw_data/2019-5-11/hesai_data_1557540003/undistorted/right_camera/1557540143.681730lf.jpg",
23-
"s3://scaleapi-cust-lidar/Hesai/raw_data/2019-5-11/hesai_data_1557540003/undistorted/front_left_camera/1557540143.639619lf.jpg",
24-
"s3://scaleapi-cust-lidar/Hesai/raw_data/2019-5-11/hesai_data_1557540003/undistorted/front_right_camera/1557540143.661212lf.jpg",
16+
"http://farm1.staticflickr.com/107/309278012_7a1f67deaa_z.jpg",
17+
"http://farm9.staticflickr.com/8001/7679588594_4e51b76472_z.jpg",
18+
"http://farm6.staticflickr.com/5295/5465771966_76f9773af1_z.jpg",
19+
"http://farm4.staticflickr.com/3449/4002348519_8ddfa4f2fb_z.jpg",
20+
"http://farm1.staticflickr.com/6/7617223_d84fcbce0e_z.jpg",
2521
]
22+
2623
TEST_DATASET_ITEMS = [
2724
DatasetItem(TEST_IMG_URLS[0], "1"),
2825
DatasetItem(TEST_IMG_URLS[1], "2"),
@@ -39,30 +36,6 @@
3936
]
4037

4138

42-
def get_signed_url(url):
43-
bucket, key = get_s3_details(url)
44-
return s3_sign(bucket, key)
45-
46-
47-
def get_s3_details(url):
48-
# Expects S3 URL format to be https://<BUCKET>.s3.amazonaws.com/<KEY>
49-
parsed = urlparse(url)
50-
bucket = parsed.netloc[: parsed.netloc.find(".")]
51-
return bucket, parsed.path[1:]
52-
53-
54-
def s3_sign(bucket, key):
55-
s3 = boto3.client("s3")
56-
return s3.generate_presigned_url(
57-
ClientMethod="get_object",
58-
Params={
59-
"Bucket": bucket,
60-
"Key": key,
61-
},
62-
ExpiresIn=PRESIGN_EXPIRY_SECONDS,
63-
)
64-
65-
6639
def reference_id_from_url(url):
6740
return Path(url).name
6841

@@ -96,20 +69,21 @@ def reference_id_from_url(url):
9669
for i in range(len(TEST_IMG_URLS))
9770
]
9871

99-
TEST_MASK_URL = "https://scale-ml.s3.amazonaws.com/home/nucleus/mscoco_masks_uint8/000000000285.png"
72+
73+
TEST_MASK_URL = "https://raw.githubusercontent.com/scaleapi/nucleus-python-client/master/tests/testdata/000000000285.png"
74+
10075
TEST_SEGMENTATION_ANNOTATIONS = [
10176
{
10277
"reference_id": reference_id_from_url(TEST_IMG_URLS[i]),
10378
"annotation_id": f"[Pytest] Segmentation Annotation Id{i}",
104-
"mask_url": get_signed_url(TEST_MASK_URL),
79+
"mask_url": TEST_MASK_URL,
10580
"annotations": [
10681
{"label": "bear", "index": 2},
10782
{"label": "grass-merged", "index": 1},
10883
],
10984
}
11085
for i in range(len(TEST_IMG_URLS))
11186
]
112-
11387
TEST_SEGMENTATION_PREDICTIONS = TEST_SEGMENTATION_ANNOTATIONS
11488

11589
TEST_BOX_PREDICTIONS = [
@@ -122,7 +96,8 @@ def reference_id_from_url(url):
12296
for i in range(len(TEST_POLYGON_ANNOTATIONS))
12397
]
12498

125-
TEST_INDEX_EMBEDDINGS_FILE = "https://scale-ml.s3.amazonaws.com/home/nucleus/pytest/pytest_embeddings_payload.json"
99+
TEST_INDEX_EMBEDDINGS_FILE = "https://raw.githubusercontent.com/scaleapi/nucleus-python-client/master/tests/testdata/pytest_embeddings_payload.json"
100+
126101

127102
# Asserts that a box annotation instance matches a dict representing its properties.
128103
# Useful to check annotation uploads/updates match.
@@ -184,4 +159,4 @@ def assert_polygon_prediction_matches_dict(
184159
assert_polygon_annotation_matches_dict(
185160
prediction_instance, prediction_dict
186161
)
187-
assert prediction_instance.confidence == prediction_dict["confidence"]
162+
assert prediction_instance.confidence == prediction_dict["confidence"]

0 commit comments

Comments
 (0)