sanmar_sdk.ftp.client ===================== .. py:module:: sanmar_sdk.ftp.client .. autoapi-nested-parse:: 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 ---------- .. autoapisummary:: sanmar_sdk.ftp.client.HOST sanmar_sdk.ftp.client.PORT sanmar_sdk.ftp.client.PRODUCT_FOLDER sanmar_sdk.ftp.client.PRODUCT_INFORMATION_FOLDER sanmar_sdk.ftp.client.ORDER_FOLDER sanmar_sdk.ftp.client.RELEASE_FOLDER sanmar_sdk.ftp.client.ACKNOWLEDGEMENT_FOLDERS Classes ------- .. autoapisummary:: sanmar_sdk.ftp.client.SanMarFTP Module Contents --------------- .. py:data:: HOST :value: 'ftp.sanmar.com' .. py:data:: PORT :value: 2200 .. py:data:: PRODUCT_FOLDER :value: 'SanMarPDD' Where SanMar keeps the product, inventory and pricing files, refreshed nightly by 6 AM Pacific. .. py:data:: PRODUCT_INFORMATION_FOLDER :value: 'SanMarPDD/SanMarPI' Where the product information web service writes the files it is asked for. .. py:data:: ORDER_FOLDER :value: 'In' .. py:data:: RELEASE_FOLDER :value: 'Release' .. py:data:: ACKNOWLEDGEMENT_FOLDERS :value: ('Holding', 'Done') Where Holding files appear, and where SanMar moves them once an order is processed. .. py:class:: 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) 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: ... :param customer_number: The SanMar customer number, which is the SFTP username. :param password: The FTP password SanMar issued. SanMar.com logins do not work here. :param host: SanMar's SFTP server. :param port: SanMar's SFTP server. :param host_key: The server's public key, to trust it without a known_hosts entry. :param known_hosts: A known_hosts file to trust instead of the system's. :param timeout: Seconds to wait for the connection. .. py:attribute:: host :value: 'ftp.sanmar.com' .. py:attribute:: port :value: 2200 .. py:method:: __enter__() -> Self Connect. .. py:method:: __exit__(exc_type: type[BaseException] | None, exc: BaseException | None, traceback: types.TracebackType | None) -> None Disconnect. .. py:method:: connect() -> None Connect and log in, verifying the server's host key. .. py:method:: close() -> None Disconnect. Safe to call more than once. .. py:property:: sftp :type: paramiko.sftp_client.SFTPClient The underlying paramiko SFTP session, for anything this class does not cover. .. py:method:: list_folder(folder: str = '') -> list[str] List a folder's entries by name, sorted. Folder names match case-insensitively. .. py:method:: find(folder: str, pattern: str) -> list[str] 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; :meth:`newest` does:: ftp.find("SanMarPDD/SanMarPI", "Brand_OGIO_*.csv") .. py:method:: newest(folder: str, pattern: str) -> str 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 ~sanmar_sdk.exceptions.NotFoundError: If no file matches. .. py:method:: resolve(path: str) -> str 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 ~sanmar_sdk.exceptions.NotFoundError: If some part of the path does not exist; the error lists what does. .. py:method:: open(path: str, *, prefetch: bool = True) -> collections.abc.Iterator[IO[bytes]] 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. .. py:method:: open_text(path: str, *, member: str | None = None, encoding: str = 'utf-8-sig') -> collections.abc.Iterator[TextIO] 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. .. py:method:: download(path: str, destination: pathlib.Path) -> pathlib.Path Copy a remote file to ``destination`` (a file, or a folder to put it in). .. py:method:: 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]] 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: ... .. py:method:: catalog() -> contextlib.AbstractContextManager[collections.abc.Iterator[sanmar_sdk.ftp.catalog.CatalogProduct]] Stream ``SanMar_SDL_N.csv``, SanMar's main product file. .. py:method:: extended_catalog() -> contextlib.AbstractContextManager[collections.abc.Iterator[sanmar_sdk.ftp.catalog.CatalogProduct]] Stream ``SanMar_EPDD.csv``: the product file, with stock across all warehouses. .. py:method:: warehouse_inventory() -> contextlib.AbstractContextManager[collections.abc.Iterator[sanmar_sdk.ftp.inventory.WarehouseInventory]] Stream ``sanmar_dip.txt``: stock by warehouse, with current prices, refreshed hourly. .. py:method:: product_information(file_name: str) -> contextlib.AbstractContextManager[collections.abc.Iterator[sanmar_sdk.ftp.catalog.ProductInformation]] Stream a file the product information web service wrote to ``SanMarPDD/SanMarPI``. .. py:method:: upload_orders(batch: str, orders: collections.abc.Sequence[sanmar_sdk.orders.PurchaseOrder]) -> sanmar_sdk.ftp.orders.OrderFiles Upload a batch of orders, without releasing them for processing. SanMar acknowledges the batch with a Holding file (see :meth:`holding`) and holds the orders until :meth:`release_orders` releases them, for up to two weeks. These are production orders; SanMar has no test environment for SFTP ordering. .. py:method:: release_orders(batch: str, po_numbers: collections.abc.Sequence[str], *, release_number: int = 1, delay: float = 5.0) -> str 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. .. py:method:: holding(batch: str) -> collections.abc.Iterator[collections.abc.Iterator[sanmar_sdk.ftp.orders.HoldingLine]] 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 ~sanmar_sdk.exceptions.NotFoundError: If there is no Holding file for the batch yet.