Metadata-Version: 2.4
Name: catalogue-server
Version: 0.4.0
Summary: Reusable Django product catalogue with a country-aware API and shop synchronization
Author: Vasudeva Server
License-Expression: MIT
Project-URL: Homepage, https://vasudevaserver.org
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django<6.0,>=5.2
Requires-Dist: Babel>=2.12
Requires-Dist: Pillow>=10
Requires-Dist: servicetrees>=0.1.0
Provides-Extra: geoip
Requires-Dist: geoip2>=5; extra == "geoip"
Provides-Extra: shops
Requires-Dist: requests>=2; extra == "shops"
Requires-Dist: beautifulsoup4>=4; extra == "shops"
Dynamic: license-file

# catalogue-server

`catalogue-server` is a reusable Django product catalogue with public pages, a
read-only JSON API, country-aware availability, and optional synchronization
from online shops.

It deliberately models products rather than a particular industry. A `Product`
has common catalogue fields plus an `attributes` JSON object for host-specific
metadata. Related variants can share a `ProductGroup`; prices and availability
can vary by country; and an `Offer` records a shop selling a product.

## Install and start

```sh
pip install catalogue-server
# Optional integrations:
pip install "catalogue-server[geoip,shops]"
```

Add the app and URLs:

```python
INSTALLED_APPS += ["catalogue_server.apps.CatalogueServerConfig"]

urlpatterns = [
    path("api/v1/", include("catalogue_server.api_urls")),
    path("products/", include("catalogue_server.urls")),
]
```

Then initialize the database:

```sh
python manage.py migrate
python manage.py runserver
```

Products and distributors can be maintained in Django admin or through a host
project's own editor. The HTML catalogue is now at `/products/`; the API is at
`/api/v1/`. See [API.md](API.md) for its complete contract.

## Data model

- `ProductGroup`: optional grouping for variants of one product.
- `Product`: title, type (`kind`), identifier, descriptions, image, arbitrary
  JSON `attributes`, default price, and publication state.
- `ProductImage`: additional images.
- `ProductCountryInfo`: country-specific price, text, and availability date.
- `Distributor`: a retailer or trade distributor and the countries it serves.
- `Offer`: a manual or scanned shop listing, including price, stock and the
  shop's own description (kept by scans, for editors and for creating products).

The package does not prescribe the meaning of `kind`, `identifier`, or
`attributes`. For example, one host might use `kind="clothing"`, a SKU, and
size/material attributes, while another chooses an entirely different schema.
Keep that interpretation, validation, and presentation in the host project.

## Settings

- `GEOIP_COUNTRY_DB`: optional GeoLite2 Country database path.
- `CATALOGUE_DEFAULT_COUNTRY`: fallback ISO country code.
- `CATALOGUE_API_CACHE_SECONDS`: API cache lifetime; default `3600`.
- `CATALOGUE_SHOP_USER_AGENT`: user agent used during shop scans.
- `CATALOGUE_SHOP_REQUEST_DELAY`: seconds between two requests of a scan; default `1`.
- `CATALOGUE_SCAN_MIN_INTERVAL_HOURS`: `scan_shops` skips a shop scanned more recently
  than this; default `12` (`0` scans every time).
- `CATALOGUE_SHOP_PARSERS`: additional parser registry entries.
- `CATALOGUE_PRODUCT_MATCHER`: dotted path to a custom matcher class.
- `CATALOGUE_API_PRODUCT_DATA`: dotted path to `(product, request) -> dict` whose keys
  are added to each product in the API, for data a site computes rather than
  stores in `attributes`.
- `CATALOGUE_PRODUCT_LIST_FILTER`: dotted path to `(request, products) -> (products,
  context)` that narrows the product list (e.g. by a language attribute); a
  `list_filters_template` in the context is shown beside the country picker.
- `CATALOGUE_NAME`: name used in page titles when the site has none.
- `CATALOGUE_PRODUCTS_NOUN`: what the products are called in counts and short
  text; default `products` (e.g. `books`).
- `CATALOGUE_PRODUCTS_TITLE`: product list heading; default the noun capitalised
  (e.g. `Books by Jane Doe`).
- `CATALOGUE_PRODUCTS_PHRASE`: used in "Where to buy …" and "<distributor>: … in
  <region>" page titles; default the noun (e.g. `Jane Doe's books`).

The default matcher conservatively uses an exact identifier, then a unique exact
title. A custom matcher receives the product queryset, implements
`match(shop_product)`, and may implement `keep_unmatched(shop_product)`. This is
the extension point for domain-specific matching rules.

## Shop synchronization

Install the `shops` extra and configure a distributor's `shop_parser` and
`shop_options`. The built-in parsers support:

| Parser | Options |
| --- | --- |
| `woocommerce` | `url`; optional `category` |
| `shopify` | `url`; optional collection `category` and fallback `currency` |
| `squarespace` | `url`; optional store `path` (default `/shop`) |
| `wix` | `url`; optional `path`, `categories`, and fallback `currency` |

Run scans periodically, e.g. once a day from cron:

```sh
python manage.py scan_shops            # --distributor <slug> for one shop, --dry-run, --verbose
```

Scans are paced so shops are not hammered: requests are spaced out
(`CATALOGUE_SHOP_REQUEST_DELAY`), a shop scanned less than
`CATALOGUE_SCAN_MIN_INTERVAL_HOURS` ago is skipped unless `--force` is given,
and a shop that answers 429 Too Many Requests is left alone until the next run
(no retries).

A parser is a callable receiving `(options, session)` and yielding
`ShopProduct` instances. Register host-specific parsers with
`CATALOGUE_SHOP_PARSERS`; wrappers may reuse a built-in platform parser and add
host-owned attributes. Set `ShopProduct.description` to the shop's product text
to keep it on the offer. Scans preserve an editor's manual product assignment.

## Optional integrations and overrides

The package uses servicetrees' `DisplayPage` for an optional richer product page;
without one, catalogue-server generates its own detail page. Standard Django
template and static-file override rules let a host supply its own design.

Image fields use ordinary Django storage. If servicetrees media storage is
configured, those files also appear in its media library.

For local package development:

```sh
pip install -e ./path/to/catalogue_server
python manage.py test catalogue_server
```

[Vasudeva Server](https://vasudevaserver.org)
