pyUSPTO 0.4.2__tar.gz → 0.4.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. {pyuspto-0.4.2 → pyuspto-0.4.3}/.github/workflows/python-tests.yml +3 -4
  2. {pyuspto-0.4.2 → pyuspto-0.4.3}/CONTRIBUTING.md +2 -0
  3. {pyuspto-0.4.2 → pyuspto-0.4.3}/PKG-INFO +20 -13
  4. {pyuspto-0.4.2 → pyuspto-0.4.3}/examples/ifw_example.py +40 -2
  5. {pyuspto-0.4.2 → pyuspto-0.4.3}/examples/patent_data_example.py +22 -0
  6. {pyuspto-0.4.2 → pyuspto-0.4.3}/pyproject.toml +9 -16
  7. {pyuspto-0.4.2 → pyuspto-0.4.3}/requirements-dev.txt +29 -19
  8. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/clients/patent_data.py +170 -14
  9. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/models/patent_data.py +21 -0
  10. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO.egg-info/PKG-INFO +20 -13
  11. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO.egg-info/requires.txt +18 -8
  12. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/clients/test_patent_data_clients.py +416 -43
  13. {pyuspto-0.4.2 → pyuspto-0.4.3}/tox.ini +2 -4
  14. {pyuspto-0.4.2 → pyuspto-0.4.3}/.github/ISSUE_TEMPLATE/bug-report.yml +0 -0
  15. {pyuspto-0.4.2 → pyuspto-0.4.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  16. {pyuspto-0.4.2 → pyuspto-0.4.3}/.github/ISSUE_TEMPLATE/documentation-updates.yml +0 -0
  17. {pyuspto-0.4.2 → pyuspto-0.4.3}/.github/ISSUE_TEMPLATE/feature-request.yml +0 -0
  18. {pyuspto-0.4.2 → pyuspto-0.4.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  19. {pyuspto-0.4.2 → pyuspto-0.4.3}/.github/dependabot.yml +0 -0
  20. {pyuspto-0.4.2 → pyuspto-0.4.3}/.github/workflows/publish-docs.yaml +0 -0
  21. {pyuspto-0.4.2 → pyuspto-0.4.3}/.github/workflows/publish-to-test-pypi.yml +0 -0
  22. {pyuspto-0.4.2 → pyuspto-0.4.3}/.gitignore +0 -0
  23. {pyuspto-0.4.2 → pyuspto-0.4.3}/.readthedocs.yaml +0 -0
  24. {pyuspto-0.4.2 → pyuspto-0.4.3}/ADVANCED.md +0 -0
  25. {pyuspto-0.4.2 → pyuspto-0.4.3}/CHANGELOG.md +0 -0
  26. {pyuspto-0.4.2 → pyuspto-0.4.3}/CODE_OF_CONDUCT.md +0 -0
  27. {pyuspto-0.4.2 → pyuspto-0.4.3}/LICENSE +0 -0
  28. {pyuspto-0.4.2 → pyuspto-0.4.3}/README.md +0 -0
  29. {pyuspto-0.4.2 → pyuspto-0.4.3}/_version.pyi +0 -0
  30. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/Makefile +0 -0
  31. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/_static/.gitkeep +0 -0
  32. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/advanced.rst +0 -0
  33. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/clients/bulk_data.rst +0 -0
  34. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/clients/index.rst +0 -0
  35. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/clients/patent_data.rst +0 -0
  36. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/clients/petition_decisions.rst +0 -0
  37. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/clients/ptab_appeals.rst +0 -0
  38. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/clients/ptab_interferences.rst +0 -0
  39. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/clients/ptab_trials.rst +0 -0
  40. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/config/config.rst +0 -0
  41. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/config/http_config.rst +0 -0
  42. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/config/index.rst +0 -0
  43. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/exceptions.rst +0 -0
  44. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/index.rst +0 -0
  45. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/models/base.rst +0 -0
  46. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/models/bulk_data.rst +0 -0
  47. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/models/index.rst +0 -0
  48. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/models/patent_data.rst +0 -0
  49. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/models/petition_decisions.rst +0 -0
  50. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/models/ptab.rst +0 -0
  51. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/models/utils.rst +0 -0
  52. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/api/warnings.rst +0 -0
  53. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/conf.py +0 -0
  54. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/configuration.rst +0 -0
  55. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/development.rst +0 -0
  56. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/examples/bulk_data.rst +0 -0
  57. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/examples/ifw_example.rst +0 -0
  58. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/examples/index.rst +0 -0
  59. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/examples/patent_data.rst +0 -0
  60. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/examples/petition_decisions.rst +0 -0
  61. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/examples/ptab_appeals.rst +0 -0
  62. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/examples/ptab_interferences.rst +0 -0
  63. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/examples/ptab_trials.rst +0 -0
  64. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/index.rst +0 -0
  65. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/installation.rst +0 -0
  66. {pyuspto-0.4.2 → pyuspto-0.4.3}/docs/source/quickstart.rst +0 -0
  67. {pyuspto-0.4.2 → pyuspto-0.4.3}/examples/bulk_data_example.py +0 -0
  68. {pyuspto-0.4.2 → pyuspto-0.4.3}/examples/error_handling_example.py +0 -0
  69. {pyuspto-0.4.2 → pyuspto-0.4.3}/examples/petition_decisions_example.py +0 -0
  70. {pyuspto-0.4.2 → pyuspto-0.4.3}/examples/ptab_appeals_example.py +0 -0
  71. {pyuspto-0.4.2 → pyuspto-0.4.3}/examples/ptab_interferences_example.py +0 -0
  72. {pyuspto-0.4.2 → pyuspto-0.4.3}/examples/ptab_trials_example.py +0 -0
  73. {pyuspto-0.4.2 → pyuspto-0.4.3}/pyUSPTO.code-workspace +0 -0
  74. {pyuspto-0.4.2 → pyuspto-0.4.3}/requirements.txt +0 -0
  75. {pyuspto-0.4.2 → pyuspto-0.4.3}/setup.cfg +0 -0
  76. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/__init__.py +0 -0
  77. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/clients/__init__.py +0 -0
  78. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/clients/base.py +0 -0
  79. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/clients/bulk_data.py +0 -0
  80. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/clients/petition_decisions.py +0 -0
  81. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/clients/ptab_appeals.py +0 -0
  82. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/clients/ptab_interferences.py +0 -0
  83. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/clients/ptab_trials.py +0 -0
  84. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/config.py +0 -0
  85. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/exceptions.py +0 -0
  86. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/http_config.py +0 -0
  87. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/models/__init__.py +0 -0
  88. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/models/base.py +0 -0
  89. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/models/bulk_data.py +0 -0
  90. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/models/petition_decisions.py +0 -0
  91. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/models/ptab.py +0 -0
  92. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/models/utils.py +0 -0
  93. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/py.typed +0 -0
  94. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/utils/__init__.py +0 -0
  95. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO/warnings.py +0 -0
  96. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO.egg-info/SOURCES.txt +0 -0
  97. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO.egg-info/dependency_links.txt +0 -0
  98. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO.egg-info/top_level.txt +0 -0
  99. {pyuspto-0.4.2 → pyuspto-0.4.3}/src/pyUSPTO.egg-info/zip-safe +0 -0
  100. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/__init__.py +0 -0
  101. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/clients/__init__.py +0 -0
  102. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/clients/test_base.py +0 -0
  103. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/clients/test_bulk_data_clients.py +0 -0
  104. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/clients/test_petition_decision_clients.py +0 -0
  105. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/clients/test_ptab_appeals_client.py +0 -0
  106. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/clients/test_ptab_interferences_client.py +0 -0
  107. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/clients/test_ptab_trials_client.py +0 -0
  108. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/conftest.py +0 -0
  109. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/integration/__init__.py +0 -0
  110. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/integration/conftest.py +0 -0
  111. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/integration/test_bulk_data_integration.py +0 -0
  112. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/integration/test_patent_data_integration.py +0 -0
  113. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/integration/test_petition_decisions_integration.py +0 -0
  114. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/integration/test_ptab_appeals_integration.py +0 -0
  115. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/integration/test_ptab_interferences_integration.py +0 -0
  116. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/integration/test_ptab_trials_integration.py +0 -0
  117. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/models/__init__.py +0 -0
  118. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/models/test_bulk_data_models.py +0 -0
  119. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/models/test_patent_data_models.py +0 -0
  120. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/models/test_petition_decision_models.py +0 -0
  121. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/models/test_ptab_models.py +0 -0
  122. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/models/test_utils.py +0 -0
  123. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/test_config.py +0 -0
  124. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/test_edge_cases.py +0 -0
  125. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/test_exceptions.py +0 -0
  126. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/test_http_config.py +0 -0
  127. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/test_init.py +0 -0
  128. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/test_version.py +0 -0
  129. {pyuspto-0.4.2 → pyuspto-0.4.3}/tests/utils/__init__.py +0 -0
@@ -25,8 +25,7 @@ jobs:
25
25
  run: |
26
26
  python -m pip install --upgrade pip
27
27
  pip install -r requirements.txt
28
- pip install -r requirements-dev.txt
29
- pip install -e .
28
+ pip install -e ".[test]"
30
29
 
31
30
  - name: Run tests
32
31
  run: |
@@ -46,12 +45,12 @@ jobs:
46
45
  - name: Set up Python
47
46
  uses: actions/setup-python@v6
48
47
  with:
49
- python-version: "3.10"
48
+ python-version: "3.14"
50
49
 
51
50
  - name: Install dependencies
52
51
  run: |
53
52
  python -m pip install --upgrade pip
54
- pip install ruff mypy
53
+ pip install -e ".[lint]"
55
54
 
56
55
  - name: Lint with ruff
57
56
  run: |
@@ -22,6 +22,8 @@ cd pyUSPTO
22
22
 
23
23
  ### Set Up Development Environment
24
24
 
25
+ > **Note:** Python 3.11+ is required for the development environment. Some dev dependencies (e.g. `myst-parser`) do not support Python 3.10. The package itself supports Python 3.10+.
26
+
25
27
  ```bash
26
28
  # Create and activate a virtual environment
27
29
  python -m venv venv
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyUSPTO
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: A Modern Python client for accessing the United States Patent and Trademark Office (USPTO) Open Data Portal (ODP) APIs.
5
5
  Author-email: Andrew Piechocki <apiechocki@dunlapcodding.com>
6
6
  License-Expression: MIT
@@ -43,19 +43,26 @@ License-File: LICENSE
43
43
  Requires-Dist: requests>=2.32.5
44
44
  Requires-Dist: typing-extensions>=4.15.0; python_version < "3.11"
45
45
  Requires-Dist: tzdata>=2025.3
46
+ Provides-Extra: test
47
+ Requires-Dist: pytest>=9.0.2; extra == "test"
48
+ Requires-Dist: pytest-cov>=7.0.0; extra == "test"
49
+ Requires-Dist: pytest-mock>=3.15.1; extra == "test"
50
+ Requires-Dist: typing_extensions>=4.15.0; extra == "test"
51
+ Provides-Extra: docs
52
+ Requires-Dist: sphinx>=9.1.0; extra == "docs"
53
+ Requires-Dist: sphinx-rtd-theme>=3.1.0; extra == "docs"
54
+ Requires-Dist: sphinx_immaterial>=0.13.9; extra == "docs"
55
+ Requires-Dist: sphinx-autodoc-typehints>=3.9.5; extra == "docs"
56
+ Requires-Dist: sphinx-copybutton>=0.5.2; extra == "docs"
57
+ Requires-Dist: myst-parser>=5.0.0; extra == "docs"
58
+ Provides-Extra: lint
59
+ Requires-Dist: mypy>=1.19.0; extra == "lint"
60
+ Requires-Dist: types-requests>=2.32.4; extra == "lint"
61
+ Requires-Dist: ruff>=0.15.0; extra == "lint"
46
62
  Provides-Extra: dev
47
- Requires-Dist: pytest>=9.0.2; extra == "dev"
48
- Requires-Dist: pytest-cov>=7.0.0; extra == "dev"
49
- Requires-Dist: pytest-mock>=3.15.1; extra == "dev"
50
- Requires-Dist: sphinx==8.1.3; extra == "dev"
51
- Requires-Dist: sphinx-rtd-theme>=3.0.0; extra == "dev"
52
- Requires-Dist: sphinx_immaterial>=0.13.8; extra == "dev"
53
- Requires-Dist: sphinx-autodoc-typehints==3.0.1; extra == "dev"
54
- Requires-Dist: sphinx-copybutton>=0.5.2; extra == "dev"
55
- Requires-Dist: myst-parser>=4.0.1; extra == "dev"
56
- Requires-Dist: mypy>=1.19.0; extra == "dev"
57
- Requires-Dist: types-requests>=2.32.4; extra == "dev"
58
- Requires-Dist: ruff>=0.15.0; extra == "dev"
63
+ Requires-Dist: pyUSPTO[test]; extra == "dev"
64
+ Requires-Dist: pyUSPTO[docs]; extra == "dev"
65
+ Requires-Dist: pyUSPTO[lint]; extra == "dev"
59
66
  Dynamic: license-file
60
67
 
61
68
  # pyUSPTO
@@ -1,7 +1,26 @@
1
1
  """Example usage of pyUSPTO for IFW data.
2
2
 
3
- This example demonstrates how to use the PatentDataClient to interact with the USPTO Patent Data API.
4
- It shows how to retrieve IFW based on various identifying values.
3
+ This example demonstrates how to use the PatentDataClient to retrieve Image File
4
+ Wrapper (IFW) data from the USPTO Patent Data API. It covers:
5
+
6
+ - get_IFW_metadata(): retrieve a PatentFileWrapper (with populated document_bag)
7
+ using any of the five supported identifiers:
8
+ - application_number
9
+ - patent_number
10
+ - publication_number
11
+ - PCT_app_number
12
+ - PCT_pub_number
13
+
14
+ - get_IFW(): retrieve metadata AND bulk-download all prosecution history documents
15
+ (PDF preferred, DOCX fallback; XML and formatless docs are skipped). Returns an
16
+ IFWResult with:
17
+ - wrapper: the PatentFileWrapper
18
+ - output_path: path to the ZIP archive (as_zip=True, default) or output directory
19
+ - downloaded_documents: dict mapping document_identifier -> filename, allowing
20
+ each Document in document_bag to be linked to its downloaded file
21
+
22
+ - download_archive() / download_publication(): download the pgpub or grant XML
23
+ archive from PrintedMetaData.
5
24
  """
6
25
 
7
26
  import json
@@ -61,6 +80,25 @@ if pct_pub_no_ifw and pct_pub_no_ifw.application_meta_data:
61
80
  print(f" - IFW Found based on PCT Pub No: {PCT_pub_number}")
62
81
 
63
82
 
83
+ print("\nGet IFW + download all prosecution docs as a ZIP archive -->")
84
+ ifw_result = client.get_IFW(application_number=application_number, destination="./download-example", overwrite=True)
85
+ if ifw_result:
86
+ print(f"Title: {ifw_result.wrapper.application_meta_data.invention_title if ifw_result.wrapper.application_meta_data else 'N/A'}")
87
+ print(f"Output: {ifw_result.output_path}")
88
+ doc_bag = ifw_result.wrapper.document_bag or []
89
+ print(f"Documents downloaded: {len(ifw_result.downloaded_documents)} of {len(doc_bag)}")
90
+ for doc in doc_bag:
91
+ if doc.document_identifier:
92
+ filename = ifw_result.downloaded_documents.get(doc.document_identifier)
93
+ status = f"-> {filename}" if filename else "(skipped)"
94
+ print(f" {doc.document_code} [{doc.document_identifier}] {status}")
95
+
96
+ print("\nGet IFW + download all prosecution docs as a directory (no ZIP) -->")
97
+ ifw_dir_result = client.get_IFW(application_number=application_number, destination="./download-example", overwrite=True, as_zip=False)
98
+ if ifw_dir_result:
99
+ print(f"Output directory: {ifw_dir_result.output_path}")
100
+
101
+
64
102
  print("\nNow let's download the Patent Publication Text -->")
65
103
  if app_no_ifw and app_no_ifw.pgpub_document_meta_data:
66
104
  pgpub_archive = app_no_ifw.pgpub_document_meta_data
@@ -302,6 +302,28 @@ except Exception as e:
302
302
  print(f"Error with POST search: {e}")
303
303
 
304
304
 
305
+ # Search by CPC classification code
306
+ # CPC codes containing spaces or slashes are automatically quoted for the Lucene query.
307
+ # cpc_classification_bag on ApplicationMetaData is a list[str] of all CPC codes assigned
308
+ # to the application, so each result may have multiple codes.
309
+ try:
310
+ print("\nSearching by CPC classification code 'H10D 64/667'...")
311
+ cpc_response = client.search_applications(
312
+ classification_q="H10D 64/667", limit=3
313
+ )
314
+ print(f"Found {cpc_response.count} applications with CPC code H10D 64/667.")
315
+ for patent_wrapper in cpc_response.patent_file_wrapper_data_bag:
316
+ app_meta = patent_wrapper.application_meta_data
317
+ if app_meta:
318
+ print(
319
+ f" - App No: {patent_wrapper.application_number_text}, Title: {app_meta.invention_title}"
320
+ )
321
+ if app_meta.cpc_classification_bag:
322
+ print(f" CPC codes: {', '.join(app_meta.cpc_classification_bag)}")
323
+ except Exception as e:
324
+ print(f"Error searching by CPC classification: {e}")
325
+
326
+
305
327
  # Example of getting status codes
306
328
  try:
307
329
  print("\nGetting first 5 status codes...")
@@ -51,24 +51,17 @@ dependencies = [
51
51
  dynamic = ["version"]
52
52
 
53
53
  [project.optional-dependencies]
54
- dev = [
55
- # Testing
56
- "pytest>=9.0.2",
57
- "pytest-cov>=7.0.0",
58
- "pytest-mock>=3.15.1",
59
- # Documentation
60
- "sphinx==8.1.3",
61
- "sphinx-rtd-theme>=3.0.0",
62
- "sphinx_immaterial>=0.13.8",
63
- "sphinx-autodoc-typehints==3.0.1",
54
+ test = ["pytest>=9.0.2", "pytest-cov>=7.0.0", "pytest-mock>=3.15.1", "typing_extensions>=4.15.0"]
55
+ docs = [
56
+ "sphinx>=9.1.0",
57
+ "sphinx-rtd-theme>=3.1.0",
58
+ "sphinx_immaterial>=0.13.9",
59
+ "sphinx-autodoc-typehints>=3.9.5",
64
60
  "sphinx-copybutton>=0.5.2",
65
- "myst-parser>=4.0.1",
66
- # Type checking
67
- "mypy>=1.19.0",
68
- "types-requests>=2.32.4",
69
- # Code quality and formatting
70
- "ruff>=0.15.0",
61
+ "myst-parser>=5.0.0",
71
62
  ]
63
+ lint = ["mypy>=1.19.0", "types-requests>=2.32.4", "ruff>=0.15.0"]
64
+ dev = ["pyUSPTO[test]", "pyUSPTO[docs]", "pyUSPTO[lint]"]
72
65
 
73
66
  [project.urls]
74
67
  GitHub = "https://github.com/DunlapCoddingPC/pyUSPTO"
@@ -22,14 +22,14 @@ colorama==0.4.6
22
22
  # sphinx
23
23
  coverage==7.13.4
24
24
  # via pytest-cov
25
- docutils==0.21.2
25
+ docutils==0.22.4
26
26
  # via
27
27
  # myst-parser
28
28
  # sphinx
29
29
  # sphinx-rtd-theme
30
30
  idna==3.11
31
31
  # via requests
32
- imagesize==1.4.1
32
+ imagesize==2.0.0
33
33
  # via sphinx
34
34
  iniconfig==2.3.0
35
35
  # via pytest
@@ -39,7 +39,7 @@ jinja2==3.1.6
39
39
  # sphinx
40
40
  librt==0.8.1
41
41
  # via mypy
42
- markdown-it-py==3.0.0
42
+ markdown-it-py==4.0.0
43
43
  # via
44
44
  # mdit-py-plugins
45
45
  # myst-parser
@@ -52,11 +52,11 @@ mdit-py-plugins==0.5.0
52
52
  mdurl==0.1.2
53
53
  # via markdown-it-py
54
54
  mypy==1.19.1
55
- # via pyUSPTO (pyproject.toml)
55
+ # via pyuspto
56
56
  mypy-extensions==1.1.0
57
57
  # via mypy
58
- myst-parser==4.0.1
59
- # via pyUSPTO (pyproject.toml)
58
+ myst-parser==5.0.0
59
+ # via pyuspto
60
60
  packaging==26.0
61
61
  # via
62
62
  # pytest
@@ -81,41 +81,48 @@ pygments==2.19.2
81
81
  # sphinx
82
82
  pytest==9.0.2
83
83
  # via
84
- # pyUSPTO (pyproject.toml)
85
84
  # pytest-cov
86
85
  # pytest-mock
86
+ # pyuspto
87
87
  pytest-cov==7.0.0
88
- # via pyUSPTO (pyproject.toml)
88
+ # via pyuspto
89
89
  pytest-mock==3.15.1
90
- # via pyUSPTO (pyproject.toml)
90
+ # via pyuspto
91
+ pyuspto @ file:///C:/Users/andrewp/Documents/GitHub/pyUSPTO
92
+ # via
93
+ # pyUSPTO (pyproject.toml)
94
+ # pyuspto
91
95
  pyyaml==6.0.3
92
96
  # via myst-parser
93
97
  requests==2.32.5
94
98
  # via
95
99
  # pyUSPTO (pyproject.toml)
100
+ # pyuspto
96
101
  # sphinx
97
102
  # sphinx-immaterial
103
+ roman-numerals==4.1.0
104
+ # via sphinx
98
105
  ruff==0.15.4
99
- # via pyUSPTO (pyproject.toml)
106
+ # via pyuspto
100
107
  snowballstemmer==3.0.1
101
108
  # via sphinx
102
- sphinx==8.1.3
109
+ sphinx==9.1.0
103
110
  # via
104
111
  # myst-parser
105
- # pyUSPTO (pyproject.toml)
112
+ # pyuspto
106
113
  # sphinx-autodoc-typehints
107
114
  # sphinx-copybutton
108
115
  # sphinx-immaterial
109
116
  # sphinx-rtd-theme
110
117
  # sphinxcontrib-jquery
111
- sphinx-autodoc-typehints==3.0.1
112
- # via pyUSPTO (pyproject.toml)
118
+ sphinx-autodoc-typehints==3.9.6
119
+ # via pyuspto
113
120
  sphinx-copybutton==0.5.2
114
- # via pyUSPTO (pyproject.toml)
121
+ # via pyuspto
115
122
  sphinx-immaterial==0.13.9
116
- # via pyUSPTO (pyproject.toml)
123
+ # via pyuspto
117
124
  sphinx-rtd-theme==3.1.0
118
- # via pyUSPTO (pyproject.toml)
125
+ # via pyuspto
119
126
  sphinxcontrib-applehelp==2.0.0
120
127
  # via sphinx
121
128
  sphinxcontrib-devhelp==2.0.0
@@ -131,19 +138,22 @@ sphinxcontrib-qthelp==2.0.0
131
138
  sphinxcontrib-serializinghtml==2.0.0
132
139
  # via sphinx
133
140
  types-requests==2.32.4.20260107
134
- # via pyUSPTO (pyproject.toml)
141
+ # via pyuspto
135
142
  typing-extensions==4.15.0
136
143
  # via
137
144
  # mypy
138
145
  # pydantic
139
146
  # pydantic-core
140
147
  # pydantic-extra-types
148
+ # pyuspto
141
149
  # sphinx-immaterial
142
150
  # typing-inspection
143
151
  typing-inspection==0.4.2
144
152
  # via pydantic
145
153
  tzdata==2025.3
146
- # via pyUSPTO (pyproject.toml)
154
+ # via
155
+ # pyUSPTO (pyproject.toml)
156
+ # pyuspto
147
157
  urllib3==2.6.3
148
158
  # via
149
159
  # requests
@@ -4,7 +4,11 @@ This module provides a client for interacting with the USPTO Patent Data API.
4
4
  It allows you to search for and retrieve patent application data.
5
5
  """
6
6
 
7
+ import dataclasses
8
+ import os
9
+ import tempfile
7
10
  import warnings
11
+ import zipfile
8
12
  from collections.abc import Iterator
9
13
  from typing import Any
10
14
 
@@ -20,6 +24,7 @@ from pyUSPTO.models.patent_data import (
20
24
  DocumentMimeType,
21
25
  EventData,
22
26
  ForeignPriority,
27
+ IFWResult,
23
28
  PatentDataResponse,
24
29
  PatentFileWrapper,
25
30
  PatentTermAdjustmentData,
@@ -121,6 +126,9 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
121
126
  # Example: "PCT/US2024/012345" -> "PCTUS2412345"
122
127
  if raw.startswith("PCT"):
123
128
  parts = raw.split("/")
129
+ if len(parts) == 1:
130
+ # Already sanitized (e.g. "PCTUS0812705"), return as-is
131
+ return raw
124
132
  if len(parts) != 3:
125
133
  raise ValueError(
126
134
  f"Invalid PCT application format: {input_number}. "
@@ -298,9 +306,12 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
298
306
  )
299
307
  q_parts.append(f"assignmentBag.assigneeBag.assigneeNameText:{v}")
300
308
  if classification_q:
301
- q_parts.append(
302
- f"applicationMetaData.cpcClassificationBag:{classification_q}"
309
+ v = (
310
+ f'"{classification_q}"'
311
+ if any(c in classification_q for c in [" ", "/"])
312
+ else classification_q
303
313
  )
314
+ q_parts.append(f"applicationMetaData.cpcClassificationBag:{v}")
304
315
  if earliestPublicationNumber_q:
305
316
  q_parts.append(
306
317
  f"applicationMetaData.earliestPublicationNumber:{earliestPublicationNumber_q}"
@@ -439,9 +450,12 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
439
450
  )
440
451
  q_parts.append(f"assignmentBag.assigneeBag.assigneeNameText:{v}")
441
452
  if classification_q:
442
- q_parts.append(
443
- f"applicationMetaData.cpcClassificationBag:{classification_q}"
453
+ v = (
454
+ f'"{classification_q}"'
455
+ if any(c in classification_q for c in [" ", "/"])
456
+ else classification_q
444
457
  )
458
+ q_parts.append(f"applicationMetaData.cpcClassificationBag:{v}")
445
459
 
446
460
  if filing_date_from_q and filing_date_to_q:
447
461
  q_parts.append(
@@ -1058,27 +1072,169 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
1058
1072
  comprehensive data if found using one of the identifiers,
1059
1073
  otherwise None.
1060
1074
  """
1075
+ wrapper = None
1061
1076
  if application_number:
1062
- return self.get_application_by_number(application_number=application_number)
1063
- if patent_number:
1077
+ wrapper = self.get_application_by_number(
1078
+ application_number=application_number
1079
+ )
1080
+ elif patent_number:
1064
1081
  pdr = self.search_applications(patent_number_q=patent_number, limit=1)
1065
1082
  if pdr.patent_file_wrapper_data_bag:
1066
- return pdr.patent_file_wrapper_data_bag[0]
1067
- if publication_number:
1083
+ wrapper = pdr.patent_file_wrapper_data_bag[0]
1084
+ elif publication_number:
1068
1085
  pdr = self.search_applications(
1069
1086
  earliestPublicationNumber_q=publication_number, limit=1
1070
1087
  )
1071
1088
  if pdr.patent_file_wrapper_data_bag:
1072
- return pdr.patent_file_wrapper_data_bag[0]
1073
- if PCT_app_number:
1074
- return self.get_application_by_number(application_number=PCT_app_number)
1075
- if PCT_pub_number:
1089
+ wrapper = pdr.patent_file_wrapper_data_bag[0]
1090
+ elif PCT_app_number:
1091
+ wrapper = self.get_application_by_number(application_number=PCT_app_number)
1092
+ elif PCT_pub_number:
1076
1093
  pdr = self.search_applications(
1077
1094
  pctPublicationNumber_q=PCT_pub_number, limit=1
1078
1095
  )
1079
1096
  if pdr.patent_file_wrapper_data_bag:
1080
- return pdr.patent_file_wrapper_data_bag[0]
1081
- return None
1097
+ wrapper = pdr.patent_file_wrapper_data_bag[0]
1098
+ if wrapper is None:
1099
+ return None
1100
+ doc_bag = self.get_application_documents(wrapper.application_number_text)
1101
+ return dataclasses.replace(wrapper, document_bag=doc_bag)
1102
+
1103
+ def get_IFW(
1104
+ self,
1105
+ *,
1106
+ application_number: str | None = None,
1107
+ publication_number: str | None = None,
1108
+ patent_number: str | None = None,
1109
+ PCT_app_number: str | None = None,
1110
+ PCT_pub_number: str | None = None,
1111
+ destination: str | None = None,
1112
+ overwrite: bool = False,
1113
+ as_zip: bool = True,
1114
+ ) -> IFWResult | None:
1115
+ """Retrieve IFW metadata and download all prosecution documents.
1116
+
1117
+ Combines `get_IFW_metadata` with a bulk download of all available prosecution
1118
+ history documents (PDF preferred, DOCX fallback). Documents with no downloadable
1119
+ format (e.g., NPL references) are silently skipped. A warning is issued only
1120
+ if a document has a download URL but the download itself fails.
1121
+
1122
+ Args:
1123
+ application_number: USPTO application number (e.g., "16123456").
1124
+ publication_number: USPTO pre-grant publication number.
1125
+ patent_number: USPTO patent number.
1126
+ PCT_app_number: PCT application number.
1127
+ PCT_pub_number: PCT publication number.
1128
+ destination: Directory for output. Defaults to current directory.
1129
+ overwrite: Whether to overwrite an existing output. Default False.
1130
+ as_zip: If True (default), package all downloads into a ZIP archive
1131
+ at ``{destination}/{app_no}_ifw.zip``. If False, download files
1132
+ directly into ``{destination}/{app_no}_ifw/``.
1133
+
1134
+ Returns:
1135
+ IFWResult with the PatentFileWrapper, the output path, and a mapping
1136
+ of document_identifier to filename for each downloaded document.
1137
+ Returns None if no application was found.
1138
+
1139
+ Raises:
1140
+ FileExistsError: If the output path already exists and overwrite=False.
1141
+ """
1142
+ wrapper = self.get_IFW_metadata(
1143
+ application_number=application_number,
1144
+ publication_number=publication_number,
1145
+ patent_number=patent_number,
1146
+ PCT_app_number=PCT_app_number,
1147
+ PCT_pub_number=PCT_pub_number,
1148
+ )
1149
+ if wrapper is None:
1150
+ return None
1151
+
1152
+ dest_dir = destination or "."
1153
+ app_no = wrapper.application_number_text or "unknown"
1154
+ downloaded_documents: dict[str, str] = {}
1155
+
1156
+ if as_zip:
1157
+ output_path = os.path.join(dest_dir, f"{app_no}_ifw.zip")
1158
+ if os.path.exists(output_path) and not overwrite:
1159
+ raise FileExistsError(
1160
+ f"ZIP archive already exists: {output_path}. Use overwrite=True to replace."
1161
+ )
1162
+ os.makedirs(dest_dir, exist_ok=True)
1163
+ with tempfile.TemporaryDirectory() as tmp_dir:
1164
+ with zipfile.ZipFile(
1165
+ output_path, "w", compression=zipfile.ZIP_DEFLATED
1166
+ ) as zf:
1167
+ for doc in wrapper.document_bag or []:
1168
+ if not doc.document_identifier:
1169
+ continue
1170
+ fmt_obj = next(
1171
+ (
1172
+ f
1173
+ for f in doc.document_formats
1174
+ if f.mime_type_identifier in ("PDF", "MS_WORD")
1175
+ and f.download_url
1176
+ ),
1177
+ None,
1178
+ )
1179
+ if fmt_obj is None or not fmt_obj.download_url:
1180
+ continue
1181
+ try:
1182
+ downloaded = self._download_and_extract(
1183
+ url=fmt_obj.download_url,
1184
+ destination=tmp_dir,
1185
+ overwrite=True,
1186
+ )
1187
+ arcname = os.path.basename(downloaded)
1188
+ zf.write(downloaded, arcname=arcname)
1189
+ downloaded_documents[doc.document_identifier] = arcname
1190
+ except Exception as exc:
1191
+ warnings.warn(
1192
+ f"Failed to download document {doc.document_identifier} "
1193
+ f"({doc.document_code}): {exc}",
1194
+ stacklevel=2,
1195
+ )
1196
+ else:
1197
+ output_path = os.path.join(dest_dir, f"{app_no}_ifw")
1198
+ if os.path.exists(output_path) and not overwrite:
1199
+ raise FileExistsError(
1200
+ f"Output directory already exists: {output_path}. Use overwrite=True to replace."
1201
+ )
1202
+ os.makedirs(output_path, exist_ok=True)
1203
+ for doc in wrapper.document_bag or []:
1204
+ if not doc.document_identifier:
1205
+ continue
1206
+ fmt_obj = next(
1207
+ (
1208
+ f
1209
+ for f in doc.document_formats
1210
+ if f.mime_type_identifier in ("PDF", "MS_WORD")
1211
+ and f.download_url
1212
+ ),
1213
+ None,
1214
+ )
1215
+ if fmt_obj is None or not fmt_obj.download_url:
1216
+ continue
1217
+ try:
1218
+ downloaded = self._download_and_extract(
1219
+ url=fmt_obj.download_url,
1220
+ destination=output_path,
1221
+ overwrite=overwrite,
1222
+ )
1223
+ downloaded_documents[doc.document_identifier] = os.path.basename(
1224
+ downloaded
1225
+ )
1226
+ except Exception as exc:
1227
+ warnings.warn(
1228
+ f"Failed to download document {doc.document_identifier} "
1229
+ f"({doc.document_code}): {exc}",
1230
+ stacklevel=2,
1231
+ )
1232
+
1233
+ return IFWResult(
1234
+ wrapper=wrapper,
1235
+ output_path=os.path.abspath(output_path),
1236
+ downloaded_documents=downloaded_documents,
1237
+ )
1082
1238
 
1083
1239
  def download_archive(
1084
1240
  self,
@@ -2048,6 +2048,7 @@ class PatentFileWrapper:
2048
2048
  pgpub_document_meta_data: `PrintedMetaData` for Pre-Grant Publication.
2049
2049
  grant_document_meta_data: `PrintedMetaData` for the granted patent.
2050
2050
  last_ingestion_date_time: Timestamp of when this data was last ingested by the API (UTC).
2051
+ document_bag: `DocumentBag` containing associated documents and their metadata.
2051
2052
  """
2052
2053
 
2053
2054
  application_number_text: str
@@ -2063,6 +2064,7 @@ class PatentFileWrapper:
2063
2064
  pgpub_document_meta_data: PrintedMetaData | None = None
2064
2065
  grant_document_meta_data: PrintedMetaData | None = None
2065
2066
  last_ingestion_date_time: datetime | None = None
2067
+ document_bag: DocumentBag | None = None
2066
2068
 
2067
2069
  @classmethod
2068
2070
  def from_dict(
@@ -2209,6 +2211,25 @@ class PatentFileWrapper:
2209
2211
  }
2210
2212
 
2211
2213
 
2214
+ @dataclass(frozen=True)
2215
+ class IFWResult:
2216
+ """Result of a get_IFW call: metadata wrapper, output path, and document map.
2217
+
2218
+ Attributes:
2219
+ wrapper: The PatentFileWrapper containing all IFW metadata and document_bag.
2220
+ output_path: Absolute path to the ZIP archive (when as_zip=True) or the
2221
+ output directory (when as_zip=False).
2222
+ downloaded_documents: Maps document_identifier to the filename of the
2223
+ downloaded file — the arcname inside the ZIP (as_zip=True) or the
2224
+ basename inside the output directory (as_zip=False). Documents that
2225
+ were skipped (no PDF/DOCX URL) or failed to download are absent.
2226
+ """
2227
+
2228
+ wrapper: PatentFileWrapper
2229
+ output_path: str
2230
+ downloaded_documents: dict[str, str]
2231
+
2232
+
2212
2233
  @dataclass(frozen=True)
2213
2234
  class PatentDataResponse:
2214
2235
  """Represents the overall response from a patent data API request.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyUSPTO
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: A Modern Python client for accessing the United States Patent and Trademark Office (USPTO) Open Data Portal (ODP) APIs.
5
5
  Author-email: Andrew Piechocki <apiechocki@dunlapcodding.com>
6
6
  License-Expression: MIT
@@ -43,19 +43,26 @@ License-File: LICENSE
43
43
  Requires-Dist: requests>=2.32.5
44
44
  Requires-Dist: typing-extensions>=4.15.0; python_version < "3.11"
45
45
  Requires-Dist: tzdata>=2025.3
46
+ Provides-Extra: test
47
+ Requires-Dist: pytest>=9.0.2; extra == "test"
48
+ Requires-Dist: pytest-cov>=7.0.0; extra == "test"
49
+ Requires-Dist: pytest-mock>=3.15.1; extra == "test"
50
+ Requires-Dist: typing_extensions>=4.15.0; extra == "test"
51
+ Provides-Extra: docs
52
+ Requires-Dist: sphinx>=9.1.0; extra == "docs"
53
+ Requires-Dist: sphinx-rtd-theme>=3.1.0; extra == "docs"
54
+ Requires-Dist: sphinx_immaterial>=0.13.9; extra == "docs"
55
+ Requires-Dist: sphinx-autodoc-typehints>=3.9.5; extra == "docs"
56
+ Requires-Dist: sphinx-copybutton>=0.5.2; extra == "docs"
57
+ Requires-Dist: myst-parser>=5.0.0; extra == "docs"
58
+ Provides-Extra: lint
59
+ Requires-Dist: mypy>=1.19.0; extra == "lint"
60
+ Requires-Dist: types-requests>=2.32.4; extra == "lint"
61
+ Requires-Dist: ruff>=0.15.0; extra == "lint"
46
62
  Provides-Extra: dev
47
- Requires-Dist: pytest>=9.0.2; extra == "dev"
48
- Requires-Dist: pytest-cov>=7.0.0; extra == "dev"
49
- Requires-Dist: pytest-mock>=3.15.1; extra == "dev"
50
- Requires-Dist: sphinx==8.1.3; extra == "dev"
51
- Requires-Dist: sphinx-rtd-theme>=3.0.0; extra == "dev"
52
- Requires-Dist: sphinx_immaterial>=0.13.8; extra == "dev"
53
- Requires-Dist: sphinx-autodoc-typehints==3.0.1; extra == "dev"
54
- Requires-Dist: sphinx-copybutton>=0.5.2; extra == "dev"
55
- Requires-Dist: myst-parser>=4.0.1; extra == "dev"
56
- Requires-Dist: mypy>=1.19.0; extra == "dev"
57
- Requires-Dist: types-requests>=2.32.4; extra == "dev"
58
- Requires-Dist: ruff>=0.15.0; extra == "dev"
63
+ Requires-Dist: pyUSPTO[test]; extra == "dev"
64
+ Requires-Dist: pyUSPTO[docs]; extra == "dev"
65
+ Requires-Dist: pyUSPTO[lint]; extra == "dev"
59
66
  Dynamic: license-file
60
67
 
61
68
  # pyUSPTO