Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.10", "3.12"]

steps:
- uses: actions/checkout@v4

- name: Set up uv
uses: astral-sh/setup-uv@v3
with:
version: "latest"

- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}

- name: Install dependencies
run: uv sync --dev

- name: Run tests with coverage
run: uv run pytest -v --cov=src --cov-report=term --cov-report=xml

lint:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up uv
uses: astral-sh/setup-uv@v3
with:
version: "latest"

- name: Install dependencies
run: uv sync --dev

- name: Run ruff linting
run: uv run ruff check src test
37 changes: 0 additions & 37 deletions .github/workflows/python-app.yml

This file was deleted.

43 changes: 43 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Release

on:
release:
types: [published]

jobs:
pypi:
name: Publish to PyPI
runs-on: ubuntu-latest

environment: pypi
permissions:
id-token: write

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up uv
uses: astral-sh/setup-uv@v3

- name: Install dependencies
run: uv sync --dev

- name: Run tests before release
run: uv run pytest -v

- name: Build package
run: uv build

- name: Verify build
run: |
ls -la dist/
uv run twine check dist/*

- name: Smoke test (wheel)
run: |
WHEEL=$(ls dist/*.whl)
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)"

- name: Publish to PyPI
run: uv publish --trusted-publishing always
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,5 @@ dmypy.json
.vscode/
local/
MANIFEST.in
pyproject.toml
setup.py
VERSION
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# casregnum

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


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

Expand Down
92 changes: 92 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
[project]
name = "casregnum"
version = "1.1.0"
description = "Python class to manage, check and sort CAS Registry Numbers® (CAS RN®)"
readme = "README.md"
authors = [
{name = "Axel Müller", email = "[email protected]"},
]
maintainers = [
{name = "Axel Müller", email = "[email protected]"},
]
requires-python = ">=3.10"
classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Operating System :: OS Independent",
"License :: OSI Approved :: MIT License",
"Topic :: Scientific/Engineering :: Chemistry",
]
license = "MIT"
license-files = ["LICENSE"]
dependencies = []

[project.urls]
Homepage = "https://github.com/molshape/CASRegistryNumbers"
Issues = "https://github.com/molshape/CASRegistryNumbers/issues"
Repository = "https://github.com/molshape/CASRegistryNumbers.git"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.uv]
dev-dependencies = [
"pytest",
"pytest-sugar",
"pytest-cov",
"ruff",
"twine",
]

[tool.pytest.ini_options]
testpaths = ["test"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"--strict-markers",
"--strict-config",
"--verbose",
]

[tool.ruff]
line-length = 88
target-version = "py310"

[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
]
ignore = [
"E501", # line too long
]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"

[tool.coverage.run]
source = ["src"]
omit = ["test/*"]

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if self.debug:",
"if settings.DEBUG",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if __name__ == .__main__.:",
]
1 change: 0 additions & 1 deletion src/__init__.py

This file was deleted.

8 changes: 8 additions & 0 deletions src/casregnum/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
__all__ = ["CAS"]
from .casregnum import CAS

try:
from importlib.metadata import version
__version__ = version("casregnum")
except ImportError:
__version__ = "unknown"
59 changes: 43 additions & 16 deletions src/casregnum.py → src/casregnum/casregnum.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,26 @@

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


class CAS:
def __init__(self, cas_rn):
"""
Class for CAS Registry Numbers® (CAS RN®) -
allows to manage, check and sort CAS Registry Numbers®

Example usage:
```python
from casregnum import CAS
caffeine = CAS("58-08-2")
print(caffeine)
```
"""
def __init__(self, cas_rn: int | str) -> None:
# case that input cas_rn is an integer
if isinstance(cas_rn, int):
self.cas_integer = cas_rn
Expand All @@ -24,38 +35,48 @@ def __init__(self, cas_rn):
# case that cas_rn is neither an integer nor a string
else:
raise TypeError(
f"Invalid CAS Registry Number format '{cas_rn}' (expected an integer (<class 'int'>) "
f"or a string (<class 'str'>), but found {type(cas_rn)})"
f"Invalid CAS Registry Number format '{cas_rn}' (expected an integer (<class 'int'>) or a string (<class 'str'>), but found {type(cas_rn)})"
)
# extract check digit = last digit of the CAS number
self.check_digit = int(str(cas_rn)[-1])

# default string output for CAS Registry Numbers
def __str__(self):
def __str__(self) -> str:
return str(self.cas_string)

# defines a representation for CAS Registry Numbers
def __repr__(self) -> str:
return f"CAS(cas_rn='{self.cas_string}')"

# defines a string format for CAS Registry Numbers
def __format__(self, format_spec):
def __format__(self, format_spec) -> str:
return f"{self.cas_string:{format_spec}}"

# checks if two CAS Registry Numbers are equal
def __eq__(self, other):
return True if self.cas_integer == other.cas_integer else False
def __eq__(self, other: object) -> bool:
if not isinstance(other, CAS):
return False
return self.cas_integer == other.cas_integer

# checks if self.cas_integer < other.cas_integer
def __lt__(self, other):
return True if self.cas_integer < other.cas_integer else False
def __lt__(self, other: object) -> bool:
if not isinstance(other, CAS):
return NotImplemented
return self.cas_integer < other.cas_integer

# Returns CAS Registry Number
@property
def cas_string(self):
def cas_string(self) -> str:
"""
Returns the CAS Registry Number as a formatted string (e.g. "58-08-2").
"""
return self.__cas_string

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

# Returns CAS Registry Number as an integer (without the hyphens)
@property
def cas_integer(self):
def cas_integer(self) -> int:
"""
Returns the CAS Registry Number as an integer (e.g. 58082).
"""
return self.__cas_integer

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

# Returns check digit of the CAS Registry Number
@property
def check_digit(self):
def check_digit(self) -> int:
"""
Returns the check digit of the CAS Registry Number (e.g. 2 for "58-08-2").
"""
return self.__check_digit

# Sets the CAS Registry Number check digit
@check_digit.setter
def check_digit(self, digit_to_test):
def check_digit(self, digit_to_test: int) -> None:
# check if the check digit fits to the CAS Number
# Source: https://www.cas.org/support/documentation/chemical-substances/checkdig
# get the CAS number without the check digit = integer value of (cas_integer/10)
Expand Down
1 change: 1 addition & 0 deletions test/test_functionality.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest

from casregnum import CAS


Expand Down
Loading
Loading