sanmar_sdk.ftp.client

SanMar’s SFTP server.

SanMar serves its data files over SFTP at ftp.sanmar.com, port 2200. The username is the SanMar customer number, and the password is the FTP password SanMar issues during onboarding, which is not the SanMar.com password the web services use.

The server’s host key is checked like any SSH server’s. Either have it in a known_hosts file, or pin it: get it once with ssh-keyscan -p 2200 ftp.sanmar.com, check it, and pass the key (ssh-rsa AAAA...) as host_key. An unknown key is refused.

Everything is read as a stream: a product file is parsed row by row as it downloads, and never held in memory or written to disk.

Attributes

HOST

PORT

PRODUCT_FOLDER

Where SanMar keeps the product, inventory and pricing files, refreshed nightly by 6 AM

PRODUCT_INFORMATION_FOLDER

Where the product information web service writes the files it is asked for.

ORDER_FOLDER

RELEASE_FOLDER

ACKNOWLEDGEMENT_FOLDERS

Where Holding files appear, and where SanMar moves them once an order is processed.

Classes

SanMarFTP

A connection to SanMar's SFTP server.

Module Contents

sanmar_sdk.ftp.client.HOST = 'ftp.sanmar.com'[source]
sanmar_sdk.ftp.client.PORT = 2200[source]
sanmar_sdk.ftp.client.PRODUCT_FOLDER = 'SanMarPDD'[source]

Where SanMar keeps the product, inventory and pricing files, refreshed nightly by 6 AM Pacific.

sanmar_sdk.ftp.client.PRODUCT_INFORMATION_FOLDER = 'SanMarPDD/SanMarPI'[source]

Where the product information web service writes the files it is asked for.

sanmar_sdk.ftp.client.ORDER_FOLDER = 'In'[source]
sanmar_sdk.ftp.client.RELEASE_FOLDER = 'Release'[source]
sanmar_sdk.ftp.client.ACKNOWLEDGEMENT_FOLDERS = ('Holding', 'Done')[source]

Where Holding files appear, and where SanMar moves them once an order is processed.

class sanmar_sdk.ftp.client.SanMarFTP(customer_number: int | str, password: str, *, host: str = HOST, port: int = PORT, host_key: str | None = None, known_hosts: pathlib.Path | None = None, timeout: float = 30.0)[source]

A connection to SanMar’s SFTP server.

Use it as a context manager, and read files inside the with block:

with SanMarFTP(123456, "ftp-password", host_key="ssh-rsa AAAA...") as ftp:
    with ftp.catalog() as products:
        for product in products:
            ...
Parameters:
  • customer_number – The SanMar customer number, which is the SFTP username.

  • password – The FTP password SanMar issued. SanMar.com logins do not work here.

  • host – SanMar’s SFTP server.

  • port – SanMar’s SFTP server.

  • host_key – The server’s public key, to trust it without a known_hosts entry.

  • known_hosts – A known_hosts file to trust instead of the system’s.

  • timeout – Seconds to wait for the connection.

host = 'ftp.sanmar.com'[source]
port = 2200[source]
__enter__() → Self[source]

Connect.

__exit__(exc_type: type[BaseException] | None, exc: BaseException | None, traceback: types.TracebackType | None) → None[source]

Disconnect.

connect() → None[source]

Connect and log in, verifying the server’s host key.

close() → None[source]

Disconnect. Safe to call more than once.

property sftp: paramiko.sftp_client.SFTPClient[source]

The underlying paramiko SFTP session, for anything this class does not cover.

list_folder(folder: str = '') → list[str][source]

List a folder’s entries by name, sorted. Folder names match case-insensitively.

find(folder: str, pattern: str) → list[str][source]

List the files in a folder whose names match a shell-style pattern, ignoring case.

Names come back sorted by name. SanMar dates the brand and category files it writes on request as MM-DD-YYYY, which does not sort by date; newest() does:

ftp.find("SanMarPDD/SanMarPI", "Brand_OGIO_*.csv")
newest(folder: str, pattern: str) → str[source]

Find the most recently dated file in a folder whose name matches a pattern.

Files are compared by the MM-DD-YYYY date SanMar puts in the names of the brand and category files it writes on request, then by name:

ftp.newest("SanMarPDD/SanMarPI", "Brand_OGIO_*.csv")
Raises:

NotFoundError – If no file matches.

resolve(path: str) → str[source]

Find a path on the server, matching each part of it case-insensitively.

SanMar’s guides spell the same file SanMar_SDL_N.csv and Sanmar_SDL_N.csv. An exact match wins over a case-insensitive one.

Raises:

NotFoundError – If some part of the path does not exist; the error lists what does.

open(path: str, *, prefetch: bool = True) → collections.abc.Iterator[IO[bytes]][source]

Open a remote file for reading, as bytes.

prefetch requests the whole file ahead of reading, which is much faster for reading straight through; turn it off for random access.

open_text(path: str, *, member: str | None = None, encoding: str = 'utf-8-sig') → collections.abc.Iterator[TextIO][source]

Open a remote text file, or a text file inside a remote zip archive.

For a .zip path, member names the file inside it; it may be left out when the archive holds a single .csv or .txt file.

download(path: str, destination: pathlib.Path) → pathlib.Path[source]

Copy a remote file to destination (a file, or a folder to put it in).

stream[R](path: str, reader: collections.abc.Callable[[TextIO], collections.abc.Iterator[R]], *, member: str | None = None, encoding: str = 'utf-8-sig') → collections.abc.Iterator[collections.abc.Iterator[R]][source]

Stream a remote file through one of this package’s readers.

The rows are read as the file downloads, and only while the with block is open:

with ftp.stream("SanMarPDD/sanmar_saleItems.txt", read_sale_items) as items:
    for item in items:
        ...
catalog() → contextlib.AbstractContextManager[collections.abc.Iterator[sanmar_sdk.ftp.catalog.CatalogProduct]][source]

Stream SanMar_SDL_N.csv, SanMar’s main product file.

extended_catalog() → contextlib.AbstractContextManager[collections.abc.Iterator[sanmar_sdk.ftp.catalog.CatalogProduct]][source]

Stream SanMar_EPDD.csv: the product file, with stock across all warehouses.

warehouse_inventory() → contextlib.AbstractContextManager[collections.abc.Iterator[sanmar_sdk.ftp.inventory.WarehouseInventory]][source]

Stream sanmar_dip.txt: stock by warehouse, with current prices, refreshed hourly.

product_information(file_name: str) → contextlib.AbstractContextManager[collections.abc.Iterator[sanmar_sdk.ftp.catalog.ProductInformation]][source]

Stream a file the product information web service wrote to SanMarPDD/SanMarPI.

upload_orders(batch: str, orders: collections.abc.Sequence[sanmar_sdk.orders.PurchaseOrder]) → sanmar_sdk.ftp.orders.OrderFiles[source]

Upload a batch of orders, without releasing them for processing.

SanMar acknowledges the batch with a Holding file (see holding()) and holds the orders until release_orders() releases them, for up to two weeks. These are production orders; SanMar has no test environment for SFTP ordering.

release_orders(batch: str, po_numbers: collections.abc.Sequence[str], *, release_number: int = 1, delay: float = 5.0) → str[source]

Release some or all of an uploaded batch’s orders for processing.

Each release of the same batch needs the next release_number. SanMar asks for a pause of several seconds between uploading a batch and releasing it; delay waits that long first. Returns the release file’s name.

holding(batch: str) → collections.abc.Iterator[collections.abc.Iterator[sanmar_sdk.ftp.orders.HoldingLine]][source]

Stream SanMar’s acknowledgement of an uploaded batch, once it exists.

SanMar writes it to Holding within about 15 minutes of the upload, and moves it to Done once the orders are processed. The file is the one whose name is the batch name followed by Holding, so batch 06-07-2022-1 never reads 06-07-2022-10Holding.txt.

Raises:

NotFoundError – If there is no Holding file for the batch yet.