pam-python 0.2.17__tar.gz → 0.2.18__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 (120) hide show
  1. {pam_python-0.2.17/pam_python.egg-info → pam_python-0.2.18}/PKG-INFO +2 -1
  2. {pam_python-0.2.17 → pam_python-0.2.18}/pam/__init__.py +1 -1
  3. {pam_python-0.2.17 → pam_python-0.2.18}/pam/api.py +9 -4
  4. {pam_python-0.2.17 → pam_python-0.2.18/pam_python.egg-info}/PKG-INFO +2 -1
  5. {pam_python-0.2.17 → pam_python-0.2.18}/pam_python.egg-info/requires.txt +1 -0
  6. {pam_python-0.2.17 → pam_python-0.2.18}/pyproject.toml +1 -0
  7. pam_python-0.2.18/tests/test_api.py +102 -0
  8. pam_python-0.2.17/tests/test_api.py +0 -44
  9. {pam_python-0.2.17 → pam_python-0.2.18}/LICENSE.txt +0 -0
  10. {pam_python-0.2.17 → pam_python-0.2.18}/README.md +0 -0
  11. {pam_python-0.2.17 → pam_python-0.2.18}/pam/cli.py +0 -0
  12. {pam_python-0.2.17 → pam_python-0.2.18}/pam/datetime_utils.py +0 -0
  13. {pam_python-0.2.17 → pam_python-0.2.18}/pam/interface_task_manager.py +0 -0
  14. {pam_python-0.2.17 → pam_python-0.2.18}/pam/logger.py +0 -0
  15. {pam_python-0.2.17 → pam_python-0.2.18}/pam/models/__init__.py +0 -0
  16. {pam_python-0.2.17 → pam_python-0.2.18}/pam/models/data_input_v2.py +0 -0
  17. {pam_python-0.2.17 → pam_python-0.2.18}/pam/models/request_command.py +0 -0
  18. {pam_python-0.2.17 → pam_python-0.2.18}/pam/models/sqlite_download.py +0 -0
  19. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/__init__.py +0 -0
  20. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/area.py +0 -0
  21. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/axis.py +0 -0
  22. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/bar.py +0 -0
  23. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/box_plot.py +0 -0
  24. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/bubble.py +0 -0
  25. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/candlestick.py +0 -0
  26. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/collection.py +0 -0
  27. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/definition.py +0 -0
  28. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/donut.py +0 -0
  29. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/errors.py +0 -0
  30. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/fields.py +0 -0
  31. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/heatmap.py +0 -0
  32. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/line.py +0 -0
  33. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/manager.py +0 -0
  34. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/metric.py +0 -0
  35. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/pie.py +0 -0
  36. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/polar_area.py +0 -0
  37. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/radar.py +0 -0
  38. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/radial_bar.py +0 -0
  39. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/range_area.py +0 -0
  40. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/range_bar.py +0 -0
  41. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/range_transform.py +0 -0
  42. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/record.py +0 -0
  43. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/report.py +0 -0
  44. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/scatter.py +0 -0
  45. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/schema.py +0 -0
  46. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/store.py +0 -0
  47. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/table.py +0 -0
  48. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/temporal.py +0 -0
  49. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/transport.py +0 -0
  50. {pam_python-0.2.17 → pam_python-0.2.18}/pam/reports/treemap.py +0 -0
  51. {pam_python-0.2.17 → pam_python-0.2.18}/pam/request_file_format.py +0 -0
  52. {pam_python-0.2.17 → pam_python-0.2.18}/pam/result_batch_uploader.py +0 -0
  53. {pam_python-0.2.17 → pam_python-0.2.18}/pam/server.py +0 -0
  54. {pam_python-0.2.17 → pam_python-0.2.18}/pam/service.py +0 -0
  55. {pam_python-0.2.17 → pam_python-0.2.18}/pam/sqlite.py +0 -0
  56. {pam_python-0.2.17 → pam_python-0.2.18}/pam/task_manager.py +0 -0
  57. {pam_python-0.2.17 → pam_python-0.2.18}/pam/temp_file_utils.py +0 -0
  58. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/buildcmd/pamb +0 -0
  59. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/buildcmd/pamb-base.sh +0 -0
  60. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/init/AGENT.md +0 -0
  61. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/init/REPORTS-th.md +0 -0
  62. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/init/REPORTS.md +0 -0
  63. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/init/dockerignore.tmpl +0 -0
  64. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/init/gitignore.tmpl +0 -0
  65. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/init/main.tmpl +0 -0
  66. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/init/pylintrc.tmpl +0 -0
  67. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/init/run_unit_test.bat +0 -0
  68. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/init/run_unit_test.ps1 +0 -0
  69. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/init/run_unit_test.sh +0 -0
  70. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/project/pip/Dockerfile +0 -0
  71. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/project/pip/requirements.txt +0 -0
  72. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/project/uv/Dockerfile +0 -0
  73. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/project/uv/pyproject.toml +0 -0
  74. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/project/uv/python-version +0 -0
  75. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/service/common/service.yaml +0 -0
  76. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/service/pandas/functions.tmpl +0 -0
  77. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/service/pandas/service.test.tmpl +0 -0
  78. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/service/pandas/service_class.tmpl +0 -0
  79. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/service/polars/functions.tmpl +0 -0
  80. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/service/polars/service.test.tmpl +0 -0
  81. {pam_python-0.2.17 → pam_python-0.2.18}/pam/templates/service/polars/service_class.tmpl +0 -0
  82. {pam_python-0.2.17 → pam_python-0.2.18}/pam/tester_task.py +0 -0
  83. {pam_python-0.2.17 → pam_python-0.2.18}/pam/utils.py +0 -0
  84. {pam_python-0.2.17 → pam_python-0.2.18}/pam_python.egg-info/SOURCES.txt +0 -0
  85. {pam_python-0.2.17 → pam_python-0.2.18}/pam_python.egg-info/dependency_links.txt +0 -0
  86. {pam_python-0.2.17 → pam_python-0.2.18}/pam_python.egg-info/entry_points.txt +0 -0
  87. {pam_python-0.2.17 → pam_python-0.2.18}/pam_python.egg-info/top_level.txt +0 -0
  88. {pam_python-0.2.17 → pam_python-0.2.18}/setup.cfg +0 -0
  89. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_cli_templates.py +0 -0
  90. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_data_error_v2.py +0 -0
  91. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_data_input_v2.py +0 -0
  92. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_data_timeout_v2.py +0 -0
  93. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_datetime_utils.py +0 -0
  94. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_range_transform.py +0 -0
  95. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_report_store.py +0 -0
  96. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_request_data_v2.py +0 -0
  97. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_result_batch_uploader.py +0 -0
  98. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_service_dataframe_output.py +0 -0
  99. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_sqlite_download_v2.py +0 -0
  100. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_task_manager_upload_token.py +0 -0
  101. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_temp_file_utils.py +0 -0
  102. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_temporal_mode.py +0 -0
  103. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_typed_area_report.py +0 -0
  104. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_typed_bar_report.py +0 -0
  105. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_typed_box_plot_report.py +0 -0
  106. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_typed_bubble_report.py +0 -0
  107. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_typed_candlestick_report.py +0 -0
  108. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_typed_donut_report.py +0 -0
  109. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_typed_heatmap_report.py +0 -0
  110. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_typed_line_report.py +0 -0
  111. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_typed_metric_report.py +0 -0
  112. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_typed_pie_report.py +0 -0
  113. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_typed_polar_area_report.py +0 -0
  114. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_typed_radar_report.py +0 -0
  115. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_typed_radial_bar_report.py +0 -0
  116. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_typed_range_area_report.py +0 -0
  117. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_typed_range_bar_report.py +0 -0
  118. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_typed_scatter_report.py +0 -0
  119. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_typed_table_report.py +0 -0
  120. {pam_python-0.2.17 → pam_python-0.2.18}/tests/test_typed_treemap_report.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pam-python
3
- Version: 0.2.17
3
+ Version: 0.2.18
4
4
  Summary: Pam Python Library
5
5
  Author-email: Narongrit Kanhanoi <narongrit@pams.ai>
6
6
  Project-URL: Homepage, https://github.com/heart/pam-python
@@ -15,6 +15,7 @@ Description-Content-Type: text/markdown
15
15
  License-File: LICENSE.txt
16
16
  Requires-Dist: Flask>=3.0.2
17
17
  Requires-Dist: requests>=2.32.3
18
+ Requires-Dist: requests-toolbelt>=1.0.0
18
19
  Requires-Dist: PyYAML>=6.0.2
19
20
  Dynamic: license-file
20
21
 
@@ -1,3 +1,3 @@
1
1
  """PAM Data Plugin framework."""
2
2
 
3
- __version__ = "0.2.17"
3
+ __version__ = "0.2.18"
@@ -2,6 +2,7 @@ from pathlib import Path
2
2
  import os
3
3
  import sqlite3
4
4
  import requests
5
+ from requests_toolbelt.multipart.encoder import MultipartEncoder
5
6
  from pam.models.sqlite_download import SQLiteDownloadResult, SQLiteDownloadStatus
6
7
  from pam.utils import log
7
8
 
@@ -36,7 +37,7 @@ class API:
36
37
 
37
38
  def http_upload(self, url: str, file_path: str, payload: dict = None) -> requests.Response | None:
38
39
  """
39
- Uploads a file to the specified URL with optional form data (payload).
40
+ Streams a file as multipart form data without buffering the whole file.
40
41
 
41
42
  :param url: The URL to upload the file to.
42
43
  :param file_path: The path to the file to be uploaded.
@@ -49,11 +50,15 @@ class API:
49
50
 
50
51
  try:
51
52
  with open(file_path, 'rb') as file:
52
- files = {'file': file}
53
+ fields = dict(payload or {})
54
+ fields['file'] = (Path(file_path).name, file)
55
+ # requests' files= buffers the entire multipart body. This encoder
56
+ # reads the open file incrementally and provides Content-Length.
57
+ body = MultipartEncoder(fields=fields)
53
58
  response = self.session.post(
54
59
  url,
55
- files=files,
56
- data=payload or {}, # optional form data
60
+ data=body,
61
+ headers={'Content-Type': body.content_type},
57
62
  timeout=300
58
63
  )
59
64
  response.raise_for_status()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pam-python
3
- Version: 0.2.17
3
+ Version: 0.2.18
4
4
  Summary: Pam Python Library
5
5
  Author-email: Narongrit Kanhanoi <narongrit@pams.ai>
6
6
  Project-URL: Homepage, https://github.com/heart/pam-python
@@ -15,6 +15,7 @@ Description-Content-Type: text/markdown
15
15
  License-File: LICENSE.txt
16
16
  Requires-Dist: Flask>=3.0.2
17
17
  Requires-Dist: requests>=2.32.3
18
+ Requires-Dist: requests-toolbelt>=1.0.0
18
19
  Requires-Dist: PyYAML>=6.0.2
19
20
  Dynamic: license-file
20
21
 
@@ -1,3 +1,4 @@
1
1
  Flask>=3.0.2
2
2
  requests>=2.32.3
3
+ requests-toolbelt>=1.0.0
3
4
  PyYAML>=6.0.2
@@ -22,6 +22,7 @@ classifiers = [
22
22
  dependencies = [
23
23
  "Flask>=3.0.2",
24
24
  "requests>=2.32.3",
25
+ "requests-toolbelt>=1.0.0",
25
26
  "PyYAML>=6.0.2",
26
27
  ]
27
28
 
@@ -0,0 +1,102 @@
1
+ import unittest
2
+ import tempfile
3
+ from pathlib import Path
4
+ from email.parser import BytesParser
5
+ from email.policy import default
6
+ from unittest import mock
7
+
8
+ import requests
9
+
10
+ from pam.api import API
11
+
12
+
13
+ class APITest(unittest.TestCase):
14
+ def test_http_upload_streams_file_and_preserves_multipart_fields(self):
15
+ api = API()
16
+ content = b'SQLite format 3\x00' * 65536
17
+ payload = {'token': 'test-token', 'file_name': 'state', 'is_shared': 'false'}
18
+ response = requests.Response()
19
+ response.status_code = 200
20
+ with tempfile.TemporaryDirectory() as directory:
21
+ path = Path(directory) / 'state.sqlite'
22
+ path.write_bytes(content)
23
+ real_open = open
24
+ opened = []
25
+
26
+ def guarded_open(*args, **kwargs):
27
+ file = real_open(*args, **kwargs)
28
+ opened.append(file)
29
+ original_read = file.read
30
+
31
+ def bounded_read(size=-1):
32
+ self.assertGreaterEqual(size, 0)
33
+ self.assertLessEqual(size, 16384)
34
+ return original_read(size)
35
+
36
+ file.read = bounded_read
37
+ return file
38
+
39
+ def post(url, **kwargs):
40
+ self.assertNotIn('files', kwargs)
41
+ prepared = requests.Request('POST', url, **{
42
+ key: kwargs[key] for key in ('data', 'headers')
43
+ }).prepare()
44
+ self.assertIs(prepared.body, kwargs['data'])
45
+ chunks = []
46
+ while chunk := prepared.body.read(16384):
47
+ chunks.append(chunk)
48
+ body = b''.join(chunks)
49
+ self.assertEqual(int(prepared.headers['Content-Length']), len(body))
50
+ message = BytesParser(policy=default).parsebytes(
51
+ ('Content-Type: ' + prepared.headers['Content-Type'] + '\r\n\r\n').encode() + body
52
+ )
53
+ parts = {part.get_param('name', header='content-disposition'): part
54
+ for part in message.iter_parts()}
55
+ for key, value in payload.items():
56
+ self.assertEqual(parts[key].get_payload(decode=True).decode(), value)
57
+ self.assertEqual(parts['file'].get_filename(), path.name)
58
+ self.assertEqual(parts['file'].get_payload(decode=True), content)
59
+ self.assertEqual(kwargs['timeout'], 300)
60
+ return response
61
+
62
+ api.session.post = mock.Mock(side_effect=post)
63
+ with mock.patch('pam.api.open', side_effect=guarded_open):
64
+ self.assertIs(api.http_upload('http://pam/sqlite_upload', str(path), payload), response)
65
+ self.assertTrue(opened[0].closed)
66
+ self.assertNotIn('file', payload)
67
+
68
+ def test_http_post_logs_error_response_body(self):
69
+ api = API()
70
+ response = requests.Response()
71
+ response.status_code = 404
72
+ response.url = "http://pam/api/dataadapter/adapter-1/log"
73
+ response._content = (
74
+ b'{"code":"NOT_FOUND","error_code":"ADAPTER_LOG_NOT_FOUND",'
75
+ b'"message":"Adapter execution log was not found"}'
76
+ )
77
+ api.session.post = mock.Mock(return_value=response)
78
+
79
+ with mock.patch("pam.api.log") as log_mock:
80
+ result = api.http_post(response.url, {"token": "secret"})
81
+
82
+ self.assertIsNone(result)
83
+ logged_message = log_mock.call_args.args[0]
84
+ self.assertIn("404 Client Error", logged_message)
85
+ self.assertIn('"error_code":"ADAPTER_LOG_NOT_FOUND"', logged_message)
86
+ self.assertNotIn("secret", logged_message)
87
+
88
+ def test_http_post_without_response_does_not_add_response_detail(self):
89
+ api = API()
90
+ api.session.post = mock.Mock(side_effect=requests.ConnectionError("offline"))
91
+
92
+ with mock.patch("pam.api.log") as log_mock:
93
+ result = api.http_post("http://pam/log", {})
94
+
95
+ self.assertIsNone(result)
96
+ logged_message = log_mock.call_args.args[0]
97
+ self.assertIn("offline", logged_message)
98
+ self.assertNotIn("Response:", logged_message)
99
+
100
+
101
+ if __name__ == "__main__":
102
+ unittest.main()
@@ -1,44 +0,0 @@
1
- import unittest
2
- from unittest import mock
3
-
4
- import requests
5
-
6
- from pam.api import API
7
-
8
-
9
- class APITest(unittest.TestCase):
10
- def test_http_post_logs_error_response_body(self):
11
- api = API()
12
- response = requests.Response()
13
- response.status_code = 404
14
- response.url = "http://pam/api/dataadapter/adapter-1/log"
15
- response._content = (
16
- b'{"code":"NOT_FOUND","error_code":"ADAPTER_LOG_NOT_FOUND",'
17
- b'"message":"Adapter execution log was not found"}'
18
- )
19
- api.session.post = mock.Mock(return_value=response)
20
-
21
- with mock.patch("pam.api.log") as log_mock:
22
- result = api.http_post(response.url, {"token": "secret"})
23
-
24
- self.assertIsNone(result)
25
- logged_message = log_mock.call_args.args[0]
26
- self.assertIn("404 Client Error", logged_message)
27
- self.assertIn('"error_code":"ADAPTER_LOG_NOT_FOUND"', logged_message)
28
- self.assertNotIn("secret", logged_message)
29
-
30
- def test_http_post_without_response_does_not_add_response_detail(self):
31
- api = API()
32
- api.session.post = mock.Mock(side_effect=requests.ConnectionError("offline"))
33
-
34
- with mock.patch("pam.api.log") as log_mock:
35
- result = api.http_post("http://pam/log", {})
36
-
37
- self.assertIsNone(result)
38
- logged_message = log_mock.call_args.args[0]
39
- self.assertIn("offline", logged_message)
40
- self.assertNotIn("Response:", logged_message)
41
-
42
-
43
- if __name__ == "__main__":
44
- unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes