4343# actual file type.
4444_FILE_FIELD = "pdf"
4545
46+ # Filename sent for raw bytes and unnamed file objects. The API recognises file
47+ # types by extension, so the upload must carry one; callers whose content is not
48+ # a PDF should pass a path or a named file object instead.
49+ _DEFAULT_FILENAME = "upload.pdf"
50+
4651_SUBACCOUNT_MAX_LEN = 50
47- _SUBACCOUNT_RE = re .compile (r"^ [A-Za-z0-9-]+$ " )
52+ _SUBACCOUNT_RE = re .compile (r"[A-Za-z0-9-]+" )
4853
4954
5055class PhotonClient :
@@ -142,7 +147,10 @@ def submit(
142147 Args:
143148 document: The document to upload — a path, an open binary file
144149 object, or raw bytes. A path is opened and closed by the
145- client; a file object is read as-is and left open.
150+ client; a file object is read as-is and left open. The API
151+ recognises file types by filename extension, so raw bytes and
152+ unnamed file objects are uploaded as ``upload.pdf``; for other
153+ file types, pass a path or a file object with a ``.name``.
146154 doctype: What kind of document this is; the API defaults to
147155 invoice. Any string is passed through, so doctypes newer than
148156 this SDK still work.
@@ -190,12 +198,12 @@ def submit(
190198 with contextlib .ExitStack () as cleanup :
191199 files : Any = None
192200 if isinstance (document , bytes ):
193- files = {_FILE_FIELD : document }
201+ files = {_FILE_FIELD : ( _DEFAULT_FILENAME , document ) }
194202 elif isinstance (document , (str , os .PathLike )):
195203 handle = cleanup .enter_context (open (document , "rb" ))
196204 files = {_FILE_FIELD : (os .path .basename (os .fspath (document )), handle )}
197205 elif document is not None :
198- files = {_FILE_FIELD : document }
206+ files = {_FILE_FIELD : ( _filename_for ( document ), document ) }
199207
200208 body = self ._transport .request_json (
201209 "POST" , SUBMIT_PATH , params = params , files = files
@@ -216,8 +224,7 @@ def retrieve(self, photon_key: str) -> dict[str, Any]:
216224
217225 Raises:
218226 ValueError: ``photon_key`` is empty — checked before any I/O.
219- NotReadyError: The document is still being processed; retry later,
220- or let ``extract()`` (Week 3) poll for you.
227+ NotReadyError: The document is still being processed; retry later.
221228 APIError: The response reported success but carried no ``data``
222229 object.
223230 PhotonError: See :meth:`Transport.request_json` for the rest of
@@ -265,5 +272,21 @@ def __repr__(self) -> str:
265272 )
266273
267274
275+ def _filename_for (document : IO [bytes ]) -> str :
276+ """The filename to upload a file object under.
277+
278+ The API recognises file types by the uploaded filename's extension, so an
279+ unnamed stream (``BytesIO``, a pipe, a fd-opened file) gets the default
280+ rather than httpx's extensionless fallback.
281+ """
282+ name = getattr (document , "name" , None )
283+ if isinstance (name , str ) and os .path .basename (name ):
284+ return os .path .basename (name )
285+ return _DEFAULT_FILENAME
286+
287+
268288def _is_valid_subaccount (subaccount : str ) -> bool :
269- return len (subaccount ) <= _SUBACCOUNT_MAX_LEN and bool (_SUBACCOUNT_RE .match (subaccount ))
289+ # fullmatch, not match: with match, "$" would accept a trailing newline.
290+ return len (subaccount ) <= _SUBACCOUNT_MAX_LEN and bool (
291+ _SUBACCOUNT_RE .fullmatch (subaccount )
292+ )
0 commit comments