Skip to content

Commit 633027c

Browse files
committed
Add release workflow for PyPI -> v1.1.0
- Add release workflow `release.yml` for GitHub Actions - Refactor CI pipeline with `ci.yml`, delete previous pipeline `python-app.yml` - Switch to `uv` for package management and dependency handling - Reorganize files for better consistency and implementation - Improve documentation by adding docstrings - Update `README.md` to show badges and current status - Bump to version 1.1.0 and implement `__version__`
1 parent ce47397 commit 633027c

File tree

11 files changed

+1002
-57
lines changed

11 files changed

+1002
-57
lines changed

.github/workflows/ci.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ${{ matrix.os }}
12+
strategy:
13+
matrix:
14+
os: [ubuntu-latest, windows-latest, macos-latest]
15+
python-version: ["3.10", "3.12"]
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Set up uv
21+
uses: astral-sh/setup-uv@v3
22+
with:
23+
version: "latest"
24+
25+
- name: Set up Python ${{ matrix.python-version }}
26+
run: uv python install ${{ matrix.python-version }}
27+
28+
- name: Install dependencies
29+
run: uv sync --dev
30+
31+
- name: Run tests with coverage
32+
run: uv run pytest -v --cov=src --cov-report=term --cov-report=xml
33+
34+
lint:
35+
runs-on: ubuntu-latest
36+
37+
steps:
38+
- uses: actions/checkout@v4
39+
40+
- name: Set up uv
41+
uses: astral-sh/setup-uv@v3
42+
with:
43+
version: "latest"
44+
45+
- name: Install dependencies
46+
run: uv sync --dev
47+
48+
- name: Run ruff linting
49+
run: uv run ruff check src tests

.github/workflows/python-app.yml

Lines changed: 0 additions & 37 deletions
This file was deleted.

.github/workflows/release.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Release
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
jobs:
8+
pypi:
9+
name: Publish to PyPI
10+
runs-on: ubuntu-latest
11+
12+
environment: pypi
13+
permissions:
14+
id-token: write
15+
16+
steps:
17+
- name: Checkout code
18+
uses: actions/checkout@v4
19+
20+
- name: Set up uv
21+
uses: astral-sh/setup-uv@v3
22+
23+
- name: Install dependencies
24+
run: uv sync --dev
25+
26+
- name: Run tests before release
27+
run: uv run pytest -v
28+
29+
- name: Build package
30+
run: uv build
31+
32+
- name: Verify build
33+
run: |
34+
ls -la dist/
35+
uv run twine check dist/*
36+
37+
- name: Smoke test (wheel)
38+
run: |
39+
WHEEL=$(ls dist/*.whl)
40+
uv run --isolated --no-project -p 3.12 --with "$WHEEL" python -c "from casregnum import CAS; print('Import successful'); caffeine=CAS('58-08-2'); print('Check Digit:', caffeine.check_digit)"
41+
42+
- name: Publish to PyPI
43+
run: uv publish --trusted-publishing always

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,5 @@ dmypy.json
133133
.vscode/
134134
local/
135135
MANIFEST.in
136-
pyproject.toml
137136
setup.py
138137
VERSION

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# casregnum
22

3-
![PyPI](https://img.shields.io/pypi/v/casregnum)
4-
![pytest](https://github.com/molshape/CASRegistryNumbers/actions/workflows/python-app.yml/badge.svg)
3+
![PyPI Version](https://img.shields.io/pypi/v/casregnum)
4+
![CI](https://github.com/molshape/CASRegistryNumbers/actions/workflows/ci.yml/badge.svg)
5+
![Python Versions](https://img.shields.io/pypi/pyversions/casregnum)
6+
![License](https://img.shields.io/github/license/molshape/casregnum) \
7+
![GitHub stars](https://img.shields.io/github/stars/molshape/casregnum)
8+
59

610
Python class to manage, check and sort CAS Registry Numbers® (CAS RN®).
711

pyproject.toml

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
[project]
2+
name = "casregnum"
3+
version = "1.1.0"
4+
description = "Python class to manage, check and sort CAS Registry Numbers® (CAS RN®)"
5+
readme = "README.md"
6+
authors = [
7+
{name = "Axel Müller", email = "[email protected]"},
8+
]
9+
maintainers = [
10+
{name = "Axel Müller", email = "[email protected]"},
11+
]
12+
requires-python = ">=3.10"
13+
classifiers = [
14+
"Programming Language :: Python :: 3.10",
15+
"Programming Language :: Python :: 3.11",
16+
"Programming Language :: Python :: 3.12",
17+
"Operating System :: OS Independent",
18+
"License :: OSI Approved :: MIT License",
19+
"Topic :: Scientific/Engineering :: Chemistry",
20+
]
21+
license = "MIT"
22+
license-files = ["LICENSE"]
23+
dependencies = []
24+
25+
[project.urls]
26+
Homepage = "https://github.com/molshape/CASRegistryNumbers"
27+
Issues = "https://github.com/molshape/CASRegistryNumbers/issues"
28+
Repository = "https://github.com/molshape/CASRegistryNumbers.git"
29+
30+
[build-system]
31+
requires = ["hatchling"]
32+
build-backend = "hatchling.build"
33+
34+
[tool.uv]
35+
dev-dependencies = [
36+
"pytest",
37+
"pytest-sugar",
38+
"pytest-cov",
39+
"ruff",
40+
"twine",
41+
]
42+
43+
[tool.pytest.ini_options]
44+
testpaths = ["test"]
45+
python_files = ["test_*.py"]
46+
python_classes = ["Test*"]
47+
python_functions = ["test_*"]
48+
addopts = [
49+
"--strict-markers",
50+
"--strict-config",
51+
"--verbose",
52+
]
53+
54+
[tool.ruff]
55+
line-length = 88
56+
target-version = "py310"
57+
58+
[tool.ruff.lint]
59+
select = [
60+
"E", # pycodestyle errors
61+
"W", # pycodestyle warnings
62+
"F", # pyflakes
63+
"I", # isort
64+
"B", # flake8-bugbear
65+
"C4", # flake8-comprehensions
66+
"UP", # pyupgrade
67+
]
68+
ignore = [
69+
"E501", # line too long
70+
]
71+
72+
[tool.ruff.format]
73+
quote-style = "double"
74+
indent-style = "space"
75+
skip-magic-trailing-comma = false
76+
line-ending = "auto"
77+
78+
[tool.coverage.run]
79+
source = ["src"]
80+
omit = ["test/*"]
81+
82+
[tool.coverage.report]
83+
exclude_lines = [
84+
"pragma: no cover",
85+
"def __repr__",
86+
"if self.debug:",
87+
"if settings.DEBUG",
88+
"raise AssertionError",
89+
"raise NotImplementedError",
90+
"if 0:",
91+
"if __name__ == .__main__.:",
92+
]

src/__init__.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/casregnum/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
__all__ = ["CAS"]
2+
from .casregnum import CAS
3+
4+
try:
5+
from importlib.metadata import version
6+
__version__ = version("casregnum")
7+
except ImportError:
8+
__version__ = "unknown"
Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,26 @@
22

33
"""
44
Class for CAS Registry Numbers® (CAS RN®)
5-
allwos to manage, check and sort CAS Registry Numbers®
5+
allows to manage, check and sort CAS Registry Numbers®
66
see https://www.cas.org/support/documentation/chemical-substances/checkdig
77
for a complete specification of the CAS Registry Numbers®
88
and the calculation method to determine the check digit
99
"""
1010

1111

1212
class CAS:
13-
def __init__(self, cas_rn):
13+
"""
14+
Class for CAS Registry Numbers® (CAS RN®) -
15+
allows to manage, check and sort CAS Registry Numbers®
16+
17+
Example usage:
18+
```python
19+
from casregnum import CAS
20+
caffeine = CAS("58-08-2")
21+
print(caffeine)
22+
```
23+
"""
24+
def __init__(self, cas_rn: int | str) -> None:
1425
# case that input cas_rn is an integer
1526
if isinstance(cas_rn, int):
1627
self.cas_integer = cas_rn
@@ -24,38 +35,48 @@ def __init__(self, cas_rn):
2435
# case that cas_rn is neither an integer nor a string
2536
else:
2637
raise TypeError(
27-
f"Invalid CAS Registry Number format '{cas_rn}' (expected an integer (<class 'int'>) "
28-
f"or a string (<class 'str'>), but found {type(cas_rn)})"
38+
f"Invalid CAS Registry Number format '{cas_rn}' (expected an integer (<class 'int'>) or a string (<class 'str'>), but found {type(cas_rn)})"
2939
)
3040
# extract check digit = last digit of the CAS number
3141
self.check_digit = int(str(cas_rn)[-1])
3242

3343
# default string output for CAS Registry Numbers
34-
def __str__(self):
44+
def __str__(self) -> str:
3545
return str(self.cas_string)
3646

47+
# defines a representation for CAS Registry Numbers
48+
def __repr__(self) -> str:
49+
return f"CAS(cas_rn='{self.cas_string}')"
50+
3751
# defines a string format for CAS Registry Numbers
38-
def __format__(self, format_spec):
52+
def __format__(self, format_spec) -> str:
3953
return f"{self.cas_string:{format_spec}}"
4054

4155
# checks if two CAS Registry Numbers are equal
42-
def __eq__(self, other):
43-
return True if self.cas_integer == other.cas_integer else False
56+
def __eq__(self, other: object) -> bool:
57+
if not isinstance(other, CAS):
58+
return False
59+
return self.cas_integer == other.cas_integer
4460

4561
# checks if self.cas_integer < other.cas_integer
46-
def __lt__(self, other):
47-
return True if self.cas_integer < other.cas_integer else False
62+
def __lt__(self, other: object) -> bool:
63+
if not isinstance(other, CAS):
64+
return NotImplemented
65+
return self.cas_integer < other.cas_integer
4866

4967
# Returns CAS Registry Number
5068
@property
51-
def cas_string(self):
69+
def cas_string(self) -> str:
70+
"""
71+
Returns the CAS Registry Number as a formatted string (e.g. "58-08-2").
72+
"""
5273
return self.__cas_string
5374

5475
# Sets CAS Registry Number
5576
# if the passed input value is a string, parse the string according to _____00-00-0
5677
# if the passed input value is an integer, create the string arrocing to _____00-00-0
5778
@cas_string.setter
58-
def cas_string(self, cas_rn):
79+
def cas_string(self, cas_rn: str) -> None:
5980
# convert (formatted) CAS string into integer
6081
if regex_cas := re.match(r"^(\d{2,7})\-(\d{2})-(\d{1})$", cas_rn):
6182
self.cas_integer = self.__cas_integer = int(
@@ -70,11 +91,14 @@ def cas_string(self, cas_rn):
7091

7192
# Returns CAS Registry Number as an integer (without the hyphens)
7293
@property
73-
def cas_integer(self):
94+
def cas_integer(self) -> int:
95+
"""
96+
Returns the CAS Registry Number as an integer (e.g. 58082).
97+
"""
7498
return self.__cas_integer
7599

76100
@cas_integer.setter
77-
def cas_integer(self, cas_rn):
101+
def cas_integer(self, cas_rn: int) -> None:
78102
# by definition, the lowest theoretical CAS number is 10-00-4,
79103
# the officially lowest CAS number on record is 35-66-5 (as of June 2019)
80104
# (Source: https://twitter.com/CASChemistry/status/1144222698740092929)
@@ -86,12 +110,15 @@ def cas_integer(self, cas_rn):
86110

87111
# Returns check digit of the CAS Registry Number
88112
@property
89-
def check_digit(self):
113+
def check_digit(self) -> int:
114+
"""
115+
Returns the check digit of the CAS Registry Number (e.g. 2 for "58-08-2").
116+
"""
90117
return self.__check_digit
91118

92119
# Sets the CAS Registry Number check digit
93120
@check_digit.setter
94-
def check_digit(self, digit_to_test):
121+
def check_digit(self, digit_to_test: int) -> None:
95122
# check if the check digit fits to the CAS Number
96123
# Source: https://www.cas.org/support/documentation/chemical-substances/checkdig
97124
# get the CAS number without the check digit = integer value of (cas_integer/10)

test/test_functionality.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pytest
2+
23
from casregnum import CAS
34

45

0 commit comments

Comments
 (0)