diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..ac617fc --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,9 @@ +{ + "image": "mcr.microsoft.com/devcontainers/universal:2", + "features": { + "ghcr.io/devcontainers/features/python:1": { + "version": "3.11" + }, + "ghcr.io/devcontainers/features/powershell:1": {} + } +} diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..5e2ddc6 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,19 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + ci-dependencies: + patterns: + - "*" + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "monthly" + groups: + python-dependencies: + patterns: + - "*" diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml new file mode 100644 index 0000000..1438149 --- /dev/null +++ b/.github/workflows/dependency-review.yaml @@ -0,0 +1,20 @@ +name: "Dependency Review" + +on: + pull_request: + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + + steps: + - name: "Checkout Repository" + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + + - name: "Dependency Review" + uses: actions/dependency-review-action@6c5ccdad469c9f8a2996bfecaec55a631a347034 # v3.1.0 + with: + config-file: darbiadev/.github/.github/dependency-review-config.yaml@f85fb2104404526f99de918714364ad1d5449f5a # v1.1.3 diff --git a/.github/workflows/python-ci.yaml b/.github/workflows/python-ci.yaml new file mode 100644 index 0000000..ba06608 --- /dev/null +++ b/.github/workflows/python-ci.yaml @@ -0,0 +1,36 @@ +name: "Python CI" + +on: + push: + branches: + - main + pull_request: + +jobs: + pre-commit: + uses: darbiadev/.github/.github/workflows/generic-precommit.yaml@068870f051676db9e2651013f7c7196ffdaeadaa # v2.0.0 + + lint: + needs: pre-commit + uses: darbiadev/.github/.github/workflows/python-lint.yaml@068870f051676db9e2651013f7c7196ffdaeadaa # v2.0.0 + + test: + needs: lint + strategy: + matrix: + os: [ ubuntu-latest ] + python-version: [ "3.11" ] + + uses: darbiadev/.github/.github/workflows/python-test.yaml@068870f051676db9e2651013f7c7196ffdaeadaa # v2.0.0 + with: + os: ${{ matrix.os }} + python-version: ${{ matrix.python-version }} + + docs: + # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages + permissions: + contents: read + pages: write + id-token: write + + uses: darbiadev/.github/.github/workflows/github-pages-python-sphinx.yaml@068870f051676db9e2651013f7c7196ffdaeadaa # v2.0.0 diff --git a/.github/workflows/python-publish-pypi.yaml b/.github/workflows/python-publish-pypi.yaml new file mode 100644 index 0000000..cd19b35 --- /dev/null +++ b/.github/workflows/python-publish-pypi.yaml @@ -0,0 +1,49 @@ +name: "Publish Python 🐍 distributions 📦 to PyPI" + +on: + release: + types: [published] + +jobs: + build-publish: + name: "Build and publish Python 🐍 distributions 📦 to PyPI" + runs-on: ubuntu-latest + permissions: + id-token: write + + steps: + - name: "Checkout repository" + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + + - name: "Set up Python 3.x" + uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + with: + python-version: "3.x" + cache: "pip" + cache-dependency-path: "pyproject.toml" + + - name: "Install pypa/build" + run: >- + python -m + pip install + build + --user + + - name: "Build a binary wheel and a source tarball" + run: >- + python -m + build + --outdir dist/ + + - name: "Upload packages" + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + with: + name: packages + path: dist + + - name: "Publish distribution 📦 to PyPI" + uses: pypa/gh-action-pypi-publish@b7f401de30cb6434a1e19f805ff006643653240e # v1.8.10 + with: + skip-existing: true + verbose: true + print-hash: true diff --git a/.gitignore b/.gitignore index 059763d..98c9b5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,12 @@ # JetBrains .idea + +# Packaging +*.egg-info +dist + +# Cache +__pycache__ + +# Docs +docs/build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..646b406 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-case-conflict + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + - id: check-json + - id: trailing-whitespace + args: [ --markdown-linebreak-ext=md ] + - id: mixed-line-ending + args: [ --fix=lf ] + - id: end-of-file-fixer diff --git a/LICENSE b/LICENSE index 941f6dd..c04e71d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Bradley Reynolds +Copyright (c) 2022 Darbia Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..bed4efb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..dc1312a --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst new file mode 100644 index 0000000..40e661b --- /dev/null +++ b/docs/source/changelog.rst @@ -0,0 +1,5 @@ +Changelog +========= + +- :release:`0.1.0 <7th October 2023>` +- :feature:`1` Initialize package diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..4b79357 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,95 @@ +"""Configuration file for the Sphinx documentation builder. + +For the full list of built-in configuration values, see the documentation: +https://www.sphinx-doc.org/en/master/usage/configuration.html +""" + +from importlib.metadata import metadata + +project_metadata = metadata("darbiadev-sands") +project: str = project_metadata["Name"] +release: str = project_metadata["Version"] +REPO_LINK: str = project_metadata["Project-URL"].replace("repository, ", "") +copyright: str = "Darbia, 2023" # noqa: A001 +author: str = "Bradley Reynolds" + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named "sphinx.ext.*") or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.linkcode", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "autoapi.extension", + "releases", +] + +autoapi_type: str = "python" +autoapi_add_toctree_entry: bool = False +autoapi_python_use_implicit_namespaces: bool = True +autoapi_dirs: list[str] = ["../../src/darbia/"] + +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} + +# Add any paths that contain templates here, relative to this directory. +templates_path: list[str] = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns: list[str] = ["_build", "Thumbs.db", ".DS_Store"] + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme: str = "furo" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path: list[str] = ["_static"] + +releases_github_path = REPO_LINK.removeprefix("https://github.com/") +releases_release_uri = f"{REPO_LINK}/releases/tag/v%s" + + +def linkcode_resolve(domain: str, info: dict) -> str: + """linkcode_resolve.""" + if domain != "py": + return None + if not info["module"]: + return None + + import importlib + import inspect + import types + + mod = importlib.import_module(info["module"]) + + val = mod + for k in info["fullname"].split("."): + val = getattr(val, k, None) + if val is None: + break + + filename = info["module"].replace(".", "/") + ".py" + + if isinstance( + val, + types.ModuleType + | types.MethodType + | types.FunctionType + | types.TracebackType + | types.FrameType + | types.CodeType, + ): + try: + lines, first = inspect.getsourcelines(val) + last = first + len(lines) - 1 + filename += f"#L{first}-L{last}" + except (OSError, TypeError): + pass + + return f"{REPO_LINK}/blob/main/src/{filename}" diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..14ab037 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,25 @@ +darbiadev-sands +=============== + +A wrapper for S&S' API. + +Module Index +------------ + +.. toctree:: + :maxdepth: 1 + + autoapi/darbia/sns/index + +.. toctree:: + :caption: Other: + :hidden: + + changelog + +Extras +------ + +* :ref:`genindex` +* :ref:`search` +* :doc:`changelog` diff --git a/make.ps1 b/make.ps1 new file mode 100644 index 0000000..07554c8 --- /dev/null +++ b/make.ps1 @@ -0,0 +1,129 @@ +<# +.SYNOPSIS +Makefile + +.DESCRIPTION +USAGE + .\make.ps1 + +COMMANDS + init install Python build tools + install-dev install local package in editable mode + update-deps update the dependencies + upgrade-deps upgrade the dependencies + lint run `pre-commit` and `black` and `ruff` + test run `pytest` + build-dist run `python -m build` + clean delete generated content + help, -? show this help message +#> +param( + [Parameter(Position = 0)] + [ValidateSet("init", "install-dev", "update-deps", "upgrade-deps", "lint", "test", "build-dist", "clean", "help")] + [string]$Command +) + +function Invoke-Help +{ + Get-Help $PSCommandPath +} + +function Invoke-Init +{ + python -m pip install --upgrade pip wheel setuptools build +} + +function Invoke-Install-Dev +{ + python -m pip install --upgrade --editable ".[dev, tests, docs]" +} + +function Invoke-Update-Deps +{ + python -m pip install --upgrade --editable ".[dev, tests, docs]" + python -m pip install --upgrade pip-tools + cd requirements + pip-compile --resolver=backtracking requirements.in --output-file requirements.txt + pip-compile --resolver=backtracking requirements-dev.in --output-file requirements-dev.txt + pip-compile --resolver=backtracking requirements-tests.in --output-file requirements-tests.txt + pip-compile --resolver=backtracking requirements-docs.in --output-file requirements-docs.txt +} + +function Invoke-Upgrade-Deps +{ + python -m pip install --upgrade pip-tools pre-commit + pre-commit autoupdate + cd requirements + pip-compile --resolver=backtracking --upgrade requirements.in --output-file requirements.txt + pip-compile --resolver=backtracking --upgrade requirements-dev.in --output-file requirements-dev.txt + pip-compile --resolver=backtracking --upgrade requirements-tests.in --output-file requirements-tests.txt + pip-compile --resolver=backtracking --upgrade requirements-docs.in --output-file requirements-docs.txt +} + +function Invoke-Lint +{ + pre-commit run --all-files + python -m black . + python -m ruff --fix . +} + +function Invoke-Test +{ + python -m pytest +} + +function Invoke-Build-Dist +{ + python -m pip install --upgrade build + python -m build +} + +function Invoke-Clean +{ + $folders = @("build", "dist") + foreach ($folder in $folders) + { + if (Test-Path $folder) + { + + Write-Verbose "Deleting $folder" + Remove-Item $folder -Recurse -Force + } + } +} + +switch ($Command) +{ + "init" { + Invoke-Init + } + "install-dev" { + Invoke-Install-Dev + } + "lint" { + Invoke-Lint + } + "update-deps" { + Invoke-Update-Deps + } + "upgrade-deps" { + Invoke-Upgrade-Deps + } + "test" { + Invoke-Test + } + "build-dist" { + Invoke-Build-Dist + } + "clean" { + Invoke-Clean + } + "help" { + Invoke-Help + } + default + { + Invoke-Init + Invoke-Install-Dev + } +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1cc79bc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,51 @@ +[project] +name = "darbiadev-sands" +version = "0.1.0" +description = "A wrapper for S&S' API." +readme = "README.md" +authors = [ + { name = "Bradley Reynolds", email = "bradley.reynolds@darbia.dev" }, +] +license = { text = "MIT" } +requires-python = ">=3.11" +dynamic = ["dependencies", "optional-dependencies"] + +[project.urls] +repository = "https://github.com/darbiadev/darbiadev-sands/" +documentation = "https://docs.darbia.dev/darbiadev-sands/" + +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.dynamic.dependencies] +file = ["requirements/requirements.txt"] + +[tool.setuptools.dynamic.optional-dependencies] +dev = { file = ["requirements/requirements-dev.txt"] } +tests = { file = ["requirements/requirements-tests.txt"] } +docs = { file = ["requirements/requirements-docs.txt"] } + +[tool.black] +target-version = ["py311"] +line-length = 120 + +[tool.ruff] +target-version = "py311" +line-length = 120 +select = ["ALL"] +ignore = [ + "N815" # (Variable `` in class scope should not be mixedCase) - TBD +] + +[tool.ruff.extend-per-file-ignores] +"docs/*" = [ + "INP001", # (File `tests/*.py` is part of an implicit namespace package. Add an `__init__.py`.) - Docs are not modules +] +"tests/*" = [ + "INP001", # (File `tests/*.py` is part of an implicit namespace package. Add an `__init__.py`.) - Tests are not modules + "S101", # (Use of `assert` detected) - Yes, that's the point +] + +[tool.ruff.pydocstyle] +convention = "numpy" diff --git a/requirements/requirements-dev.in b/requirements/requirements-dev.in new file mode 100644 index 0000000..a30e571 --- /dev/null +++ b/requirements/requirements-dev.in @@ -0,0 +1,7 @@ +# Constrain versions installed to be compatible with core dependencies +--constraint requirements.txt + +pip-tools +pre-commit +black +ruff diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt new file mode 100644 index 0000000..a01c6f5 --- /dev/null +++ b/requirements/requirements-dev.txt @@ -0,0 +1,54 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --output-file=requirements-dev.txt requirements-dev.in +# +black==23.9.1 + # via -r requirements-dev.in +build==1.0.3 + # via pip-tools +cfgv==3.4.0 + # via pre-commit +click==8.1.7 + # via + # black + # pip-tools +distlib==0.3.7 + # via virtualenv +filelock==3.12.4 + # via virtualenv +identify==2.5.30 + # via pre-commit +mypy-extensions==1.0.0 + # via black +nodeenv==1.8.0 + # via pre-commit +packaging==23.2 + # via + # black + # build +pathspec==0.11.2 + # via black +pip-tools==7.3.0 + # via -r requirements-dev.in +platformdirs==3.11.0 + # via + # black + # virtualenv +pre-commit==3.4.0 + # via -r requirements-dev.in +pyproject-hooks==1.0.0 + # via build +pyyaml==6.0.1 + # via pre-commit +ruff==0.0.292 + # via -r requirements-dev.in +virtualenv==20.24.5 + # via pre-commit +wheel==0.41.2 + # via pip-tools + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/requirements/requirements-docs.in b/requirements/requirements-docs.in new file mode 100644 index 0000000..fc51bbd --- /dev/null +++ b/requirements/requirements-docs.in @@ -0,0 +1,7 @@ +# Constrain versions installed to be compatible with core dependencies +--constraint requirements.txt + +sphinx +furo +sphinx-autoapi +releases diff --git a/requirements/requirements-docs.txt b/requirements/requirements-docs.txt new file mode 100644 index 0000000..5313b18 --- /dev/null +++ b/requirements/requirements-docs.txt @@ -0,0 +1,86 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --output-file=requirements-docs.txt requirements-docs.in +# +alabaster==0.7.13 + # via sphinx +anyascii==0.3.2 + # via sphinx-autoapi +astroid==3.0.0 + # via sphinx-autoapi +babel==2.13.0 + # via sphinx +beautifulsoup4==4.12.2 + # via furo +certifi==2023.7.22 + # via + # -c requirements.txt + # requests +charset-normalizer==3.3.0 + # via requests +docutils==0.20.1 + # via sphinx +furo==2023.9.10 + # via -r requirements-docs.in +idna==3.4 + # via + # -c requirements.txt + # requests +imagesize==1.4.1 + # via sphinx +jinja2==3.1.2 + # via + # sphinx + # sphinx-autoapi +markupsafe==2.1.3 + # via jinja2 +packaging==23.2 + # via sphinx +pygments==2.16.1 + # via + # furo + # sphinx +pyyaml==6.0.1 + # via sphinx-autoapi +releases==2.1.1 + # via -r requirements-docs.in +requests==2.31.0 + # via sphinx +semantic-version==2.6.0 + # via releases +snowballstemmer==2.2.0 + # via sphinx +soupsieve==2.5 + # via beautifulsoup4 +sphinx==7.2.6 + # via + # -r requirements-docs.in + # furo + # releases + # sphinx-autoapi + # sphinx-basic-ng + # sphinxcontrib-applehelp + # sphinxcontrib-devhelp + # sphinxcontrib-htmlhelp + # sphinxcontrib-qthelp + # sphinxcontrib-serializinghtml +sphinx-autoapi==3.0.0 + # via -r requirements-docs.in +sphinx-basic-ng==1.0.0b2 + # via furo +sphinxcontrib-applehelp==1.0.7 + # via sphinx +sphinxcontrib-devhelp==1.0.5 + # via sphinx +sphinxcontrib-htmlhelp==2.0.4 + # via sphinx +sphinxcontrib-jsmath==1.0.1 + # via sphinx +sphinxcontrib-qthelp==1.0.6 + # via sphinx +sphinxcontrib-serializinghtml==1.1.9 + # via sphinx +urllib3==2.0.6 + # via requests diff --git a/requirements/requirements-tests.in b/requirements/requirements-tests.in new file mode 100644 index 0000000..ef23890 --- /dev/null +++ b/requirements/requirements-tests.in @@ -0,0 +1,5 @@ +# Constrain versions installed to be compatible with core dependencies +--constraint requirements.txt + +pytest +pytest-randomly diff --git a/requirements/requirements-tests.txt b/requirements/requirements-tests.txt new file mode 100644 index 0000000..d365486 --- /dev/null +++ b/requirements/requirements-tests.txt @@ -0,0 +1,18 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --output-file=requirements-tests.txt requirements-tests.in +# +iniconfig==2.0.0 + # via pytest +packaging==23.2 + # via pytest +pluggy==1.3.0 + # via pytest +pytest==7.4.2 + # via + # -r requirements-tests.in + # pytest-randomly +pytest-randomly==3.15.0 + # via -r requirements-tests.in diff --git a/requirements/requirements.in b/requirements/requirements.in new file mode 100644 index 0000000..79b5403 --- /dev/null +++ b/requirements/requirements.in @@ -0,0 +1,6 @@ +# Requirements +# --index-url=http://pypi.example.com/simple +# --no-cache-dir + +# Core +httpx diff --git a/requirements/requirements.txt b/requirements/requirements.txt new file mode 100644 index 0000000..0c7d521 --- /dev/null +++ b/requirements/requirements.txt @@ -0,0 +1,27 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --output-file=requirements.txt requirements.in +# +anyio==4.0.0 + # via httpcore +certifi==2023.7.22 + # via + # httpcore + # httpx +h11==0.14.0 + # via httpcore +httpcore==0.18.0 + # via httpx +httpx==0.25.0 + # via -r requirements.in +idna==3.4 + # via + # anyio + # httpx +sniffio==1.3.0 + # via + # anyio + # httpcore + # httpx diff --git a/src/darbia/sns/__init__.py b/src/darbia/sns/__init__.py new file mode 100644 index 0000000..b93d4d9 --- /dev/null +++ b/src/darbia/sns/__init__.py @@ -0,0 +1,10 @@ +"""A wrapper for S&S' API.""" + +from .models import Product, Warehouse +from .sns_services import SandSServices + +__all__ = [ + "Product", + "SandSServices", + "Warehouse", +] diff --git a/src/darbia/sns/models.py b/src/darbia/sns/models.py new file mode 100644 index 0000000..c4d41bc --- /dev/null +++ b/src/darbia/sns/models.py @@ -0,0 +1,139 @@ +"""Models.""" + +from dataclasses import dataclass +from typing import Self + + +@dataclass(frozen=True) +class Warehouse: + """Warehouse.""" + + warehouseAbbr: str + """Code identifying the Warehouse.""" + skuID: int + """skuID identifying the Sku and Warehouse.""" + qty: int + """Quantity available for sale.""" + closeout: bool + """Skus that are discontinued and will not be replenished.""" + dropship: bool + """This product does not ship from our warehouse.""" + excludeFreeFreight: bool + """ This product does not qualify for free freight.""" + fullCaseOnly: bool + """This product must be ordered in full case quantities.""" + returnable: bool + """This product is eligible for return.""" + expectedInventory: str | None = None + """Current enroute quantities with expected dates of receipt and current quantity on order with the mill. If no dates are available, None will be returned.""" # noqa: E501 + + @classmethod + def from_api_data(cls: type[Self], data: dict) -> Self: + """Build an instance from an API response.""" + return cls(**data) + + +@dataclass(frozen=True) +class Product: + """Product.""" + + sku: str + """Our sku number""" + gtin: str + """Our sku number""" + skuID_Master: int + """NO EXPLANATION""" + yourSku: str + """YourSku has been set up using the CrossRef API.""" + styleID: int + """Unique ID for this style (Will never change)""" + brandName: str + """The brand that makes this style.""" + styleName: str + """The style's name. Style names are unique within a brand.""" + colorName: str + """The style's name. Style names are unique within a brand.""" + colorCode: str + """Two digit color code part of the InventoryKey.""" + colorPriceCodeName: str + """The pricing category of this color.""" + colorGroup: str + """Colors with a similar color group are considered to be a similar color.""" + colorGroupName: str + """Colors with a similar color group are considered to be a similar color.""" + colorFamilyID: str + """Base color the color falls under.""" + colorFamily: str + """Base color the color falls under.""" + colorSwatchImage: str + """URL to the medium swatch image for this color""" + colorSwatchTextColor: str + """Html color code that is visible on top of the color swatch""" + colorFrontImage: str + """URL to the medium front image for this color""" + colorSideImage: str + """URL to the medium side image for this color""" + colorBackImage: str + """URL to the medium back image for this color""" + colorDirectSideImage: str + """URL to the medium direct side image for this color""" + colorOnModelFrontImage: str + """URL to the medium direct side image for this color""" + colorOnModelSideImage: str + """URL to the medium direct side image for this color""" + colorOnModelBackImage: str + """URL to the medium on model back image for this color""" + color1: str + """HTML Code for the primary color.""" + color2: str + """HTML Code for the secondary color.""" + sizeName: str + """Size Name that the spec belongs to.""" + sizeCode: str + """One digit size code part of the InventoryKey.""" + sizeOrder: str + """Sort order for the size compared to other sizes in the style.""" + sizePriceCodeName: str + """The pricing category of this size.""" + caseQty: int + """Number of units in a full case from the mill.""" + unitWeight: float + """Weight of a single unit.""" + mapPrice: float + """Minimum Advertised Price price""" + piecePrice: float + """Piece price level price""" + dozenPrice: float + """Dozen price level price""" + casePrice: float + """Case price level price""" + salePrice: float + """Sale price level price""" + customerPrice: float + """Your price""" + noeRetailing: bool + """When true, mill prohibits the selling of products on popular eRetailing platforms such as Amazon, Walmart, EBay.""" # noqa: E501 + caseWeight: int + """Weight of full case in pounds""" + caseWidth: int + """Width of case in inches""" + caseLength: float + """Width of case in inches""" + caseHeight: float + """Height of case in inches""" + polyPackQty: str + """Number of pieces in a poly pack""" + qty: int + """Combined Inventory in all of our warehouses""" + countryOfOrigin: str + """Country of manufacture for product. Provided by mills.""" + warehouses: list[Warehouse] + """List of Object""" + saleExpiration: str | None = None + """Your price""" + + @classmethod + def from_api_data(cls: type[Self], data: dict) -> Self: + """Build an instance from an API response.""" + data["warehouses"] = [Warehouse.from_api_data(row) for row in data["warehouses"]] + return cls(**data) diff --git a/src/darbia/sns/sns_services.py b/src/darbia/sns/sns_services.py new file mode 100644 index 0000000..43e29b2 --- /dev/null +++ b/src/darbia/sns/sns_services.py @@ -0,0 +1,66 @@ +"""Interacting with S&S' API.""" + +import uuid +from typing import Any, Self + +from httpx import Client + +from .models import Product + + +class SandSServices: + """A class wrapping S&S' API.""" + + def __init__( + self: Self, + base_url: str, + account_number: str, + token: str, + ) -> None: + try: + int(account_number) + except ValueError as exception: + msg = "Account number is not a valid number!" + raise TypeError(msg) from exception + + try: + uuid.UUID(token) + except ValueError as exception: + msg = "Token is not a valid UUID!" + raise TypeError(msg) from exception + + self.client = Client(auth=(account_number, token)) + self.base_url = base_url + + def make_request( # noqa: PLR0913 + self: Self, + method: str, + path: str, + params: dict[str, Any] | None = None, + json: dict[str, Any] | None = None, + timeout: int | None = None, + ) -> dict: + """Make a request to S&S' API.""" + args = { + "method": method, + "url": self.base_url + path, + } + + if params is not None: + args["params"] = params + + if json is not None: + args["json"] = json + + if timeout is not None: + args["timeout"] = timeout + + response = self.client.request(**args) + return response.json() + + def get_products( + self: Self, + ) -> list[Product]: + """Get all products.""" + product_data = self.make_request("GET", "/products", timeout=500) + return [Product.from_api_data(data) for data in product_data] diff --git a/src/darbiadev_sns/__init__.py b/src/darbiadev_sns/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/darbiadev_sns/sns_services.py b/src/darbiadev_sns/sns_services.py deleted file mode 100644 index e15b78c..0000000 --- a/src/darbiadev_sns/sns_services.py +++ /dev/null @@ -1,9 +0,0 @@ -"""SNSServices""" - -import pandas -import requests - -if __name__ == "__main__": - url = "https://api.ssactivewear.com/V2/products" - response = requests.get(url, auth=("account_number", "token")).json() - pandas.DataFrame(response).to_excel("products.xlsx") diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..5ece3b1 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,14 @@ +"""Testing the client.""" + +import pytest +from darbia.sns import SandSServices + + +def test_rejects_invalid_auth() -> None: + """Test that the client will recognize invalid auth.""" + with pytest.raises(TypeError) as excinfo: + SandSServices(base_url="", account_number="", token="") + assert "Account number" in str(excinfo.value) + with pytest.raises(TypeError) as excinfo: + SandSServices(base_url="", account_number="2", token="") + assert "Token" in str(excinfo.value)