google-form-poster 0.1.1__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.
- google_form_poster-0.1.1/.gitignore +14 -0
- google_form_poster-0.1.1/.python-version +1 -0
- google_form_poster-0.1.1/LICENSE +0 -0
- google_form_poster-0.1.1/PKG-INFO +73 -0
- google_form_poster-0.1.1/README.md +48 -0
- google_form_poster-0.1.1/pyproject.toml +49 -0
- google_form_poster-0.1.1/src/post_to_google_form/__init__.py +8 -0
- google_form_poster-0.1.1/src/post_to_google_form/_submit.py +66 -0
- google_form_poster-0.1.1/src/post_to_google_form/_url.py +22 -0
- google_form_poster-0.1.1/src/post_to_google_form/cli.py +78 -0
- google_form_poster-0.1.1/tests/__init__.py +0 -0
- google_form_poster-0.1.1/tests/test_main.py +169 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: google-form-poster
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Submit data rows to Google Forms via POST requests
|
|
5
|
+
Project-URL: Homepage, https://github.com/obabawale/google-form-poster.git
|
|
6
|
+
Project-URL: Source, https://github.com/obabawale/google-form-poster.git
|
|
7
|
+
Author-email: Olami <olami@example.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: automation,form-submission,google-forms
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: pandas>=2.0
|
|
20
|
+
Requires-Dist: python-dotenv>=1.0
|
|
21
|
+
Requires-Dist: requests>=2.31
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Google Form Poster
|
|
27
|
+
|
|
28
|
+
Submit data rows to a Google Form via POST requests.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install google-form-poster
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage — as a library
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from post_to_google_form import post_data
|
|
40
|
+
|
|
41
|
+
url = "https://docs.google.com/forms/d/e/.../viewform"
|
|
42
|
+
|
|
43
|
+
# Data keys match entry IDs directly
|
|
44
|
+
data = [{"entry.12345": "Alice", "entry.67890": "alice@example.com"}]
|
|
45
|
+
results = post_data(url, data)
|
|
46
|
+
|
|
47
|
+
# Or use key_mappings to translate your keys
|
|
48
|
+
key_mappings = {"name": "entry.12345", "email": "entry.67890"}
|
|
49
|
+
data = [{"name": "Alice", "email": "alice@example.com"}]
|
|
50
|
+
results = post_data(url, data, key_mappings)
|
|
51
|
+
|
|
52
|
+
# results = [
|
|
53
|
+
# {"index": 0, "status_code": 200, "payload": {"entry.12345": "Alice", ...}},
|
|
54
|
+
# ]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`forms.gle` short links are automatically resolved:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
post_data("https://forms.gle/abc123", data)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage — CLI
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
post-to-google-form --file data.json --url https://forms.gle/abc123
|
|
67
|
+
|
|
68
|
+
# With key mappings:
|
|
69
|
+
post-to-google-form --file data.json --url https://forms.gle/abc123 \
|
|
70
|
+
--key-mappings mappings.json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
All flags fall back to environment variables (`FILE_PATH`, `GOOGLE_FORM_URL`, `KEY_MAPPINGS`), loaded from `.env` if present.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Google Form Poster
|
|
2
|
+
|
|
3
|
+
Submit data rows to a Google Form via POST requests.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install google-form-poster
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage — as a library
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from post_to_google_form import post_data
|
|
15
|
+
|
|
16
|
+
url = "https://docs.google.com/forms/d/e/.../viewform"
|
|
17
|
+
|
|
18
|
+
# Data keys match entry IDs directly
|
|
19
|
+
data = [{"entry.12345": "Alice", "entry.67890": "alice@example.com"}]
|
|
20
|
+
results = post_data(url, data)
|
|
21
|
+
|
|
22
|
+
# Or use key_mappings to translate your keys
|
|
23
|
+
key_mappings = {"name": "entry.12345", "email": "entry.67890"}
|
|
24
|
+
data = [{"name": "Alice", "email": "alice@example.com"}]
|
|
25
|
+
results = post_data(url, data, key_mappings)
|
|
26
|
+
|
|
27
|
+
# results = [
|
|
28
|
+
# {"index": 0, "status_code": 200, "payload": {"entry.12345": "Alice", ...}},
|
|
29
|
+
# ]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`forms.gle` short links are automatically resolved:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
post_data("https://forms.gle/abc123", data)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage — CLI
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
post-to-google-form --file data.json --url https://forms.gle/abc123
|
|
42
|
+
|
|
43
|
+
# With key mappings:
|
|
44
|
+
post-to-google-form --file data.json --url https://forms.gle/abc123 \
|
|
45
|
+
--key-mappings mappings.json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
All flags fall back to environment variables (`FILE_PATH`, `GOOGLE_FORM_URL`, `KEY_MAPPINGS`), loaded from `.env` if present.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "google-form-poster"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Submit data rows to Google Forms via POST requests"
|
|
9
|
+
license = { text = "MIT" }
|
|
10
|
+
license-files = ["LICENSE"]
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Olami", email = "olami@example.com" },
|
|
15
|
+
]
|
|
16
|
+
keywords = ["google-forms", "automation", "form-submission"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"pandas>=2.0",
|
|
28
|
+
"requests>=2.31",
|
|
29
|
+
"python-dotenv>=1.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/obabawale/google-form-poster.git"
|
|
34
|
+
Source = "https://github.com/obabawale/google-form-poster.git"
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
dev = ["pytest>=8.0"]
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
post-to-google-form = "post_to_google_form.cli:main"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/post_to_google_form"]
|
|
44
|
+
|
|
45
|
+
[tool.hatch.metadata]
|
|
46
|
+
allow-direct-references = true
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from post_to_google_form._url import get_form_response_url
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_payload(
|
|
12
|
+
row: dict[str, Any],
|
|
13
|
+
key_mappings: dict[str, str] | None = None,
|
|
14
|
+
) -> dict[str, str]:
|
|
15
|
+
"""Map a row's keys to Google Form entry IDs.
|
|
16
|
+
|
|
17
|
+
If *key_mappings* is provided, only mapped keys are kept and renamed.
|
|
18
|
+
Otherwise the row is returned as-is (keys are assumed to be entry IDs).
|
|
19
|
+
"""
|
|
20
|
+
if key_mappings is None:
|
|
21
|
+
return {k: str(v) for k, v in row.items()}
|
|
22
|
+
|
|
23
|
+
return {key_mappings[k]: str(v) for k, v in row.items() if k in key_mappings}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def post_data(
|
|
27
|
+
url: str,
|
|
28
|
+
data: list[dict[str, Any]] | pd.DataFrame,
|
|
29
|
+
key_mappings: dict[str, str] | None = None,
|
|
30
|
+
) -> list[dict[str, Any]]:
|
|
31
|
+
"""Submit data rows to a Google Form.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
url:
|
|
36
|
+
Google Form URL — a ``viewform``, ``formResponse``, or ``forms.gle``
|
|
37
|
+
short link. It will be resolved to the correct endpoint automatically.
|
|
38
|
+
data:
|
|
39
|
+
Rows to submit. Each dict's keys should match the keys in
|
|
40
|
+
*key_mappings* (or be entry IDs directly if no mappings are given).
|
|
41
|
+
key_mappings:
|
|
42
|
+
Optional mapping from your data keys to Google Form entry IDs.
|
|
43
|
+
``{"name": "entry.12345", "email": "entry.67890"}``
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
list[dict[str, Any]]
|
|
48
|
+
One dict per row with keys ``"index"``, ``"status_code"``, and
|
|
49
|
+
``"payload"`` (the data that was actually POSTed).
|
|
50
|
+
"""
|
|
51
|
+
submit_url = get_form_response_url(url)
|
|
52
|
+
|
|
53
|
+
if isinstance(data, pd.DataFrame):
|
|
54
|
+
data = data.to_dict(orient="records")
|
|
55
|
+
|
|
56
|
+
results: list[dict[str, Any]] = []
|
|
57
|
+
for i, row in enumerate(data):
|
|
58
|
+
payload = build_payload(row, key_mappings)
|
|
59
|
+
resp = requests.post(submit_url, data=payload, timeout=20)
|
|
60
|
+
results.append({
|
|
61
|
+
"index": i,
|
|
62
|
+
"status_code": resp.status_code,
|
|
63
|
+
"payload": payload,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
return results
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_form_response_url(url: str) -> str:
|
|
9
|
+
"""Resolve a Google Forms view URL to its formResponse endpoint."""
|
|
10
|
+
parsed = urlparse(url)
|
|
11
|
+
|
|
12
|
+
if "forms.gle" in parsed.netloc:
|
|
13
|
+
resolved = requests.get(url, allow_redirects=True, timeout=15).url
|
|
14
|
+
parsed = urlparse(resolved)
|
|
15
|
+
|
|
16
|
+
if parsed.path.endswith("/viewform"):
|
|
17
|
+
return f"{parsed.scheme}://{parsed.netloc}{parsed.path.replace('/viewform', '/formResponse')}"
|
|
18
|
+
|
|
19
|
+
if parsed.path.endswith("/formResponse"):
|
|
20
|
+
return url
|
|
21
|
+
|
|
22
|
+
return f"{parsed.scheme}://{parsed.netloc}{parsed.path.rstrip('/')}/formResponse"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
|
|
11
|
+
from post_to_google_form._submit import build_payload, post_data
|
|
12
|
+
from post_to_google_form._url import get_form_response_url
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
description="Submit data rows from a JSON file to a Google Form."
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"--file",
|
|
21
|
+
help="Path to the JSON data file (default: $FILE_PATH)",
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--url",
|
|
25
|
+
help="Google Form URL (default: $GOOGLE_FORM_URL)",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--key-mappings",
|
|
29
|
+
help="Path to a JSON file mapping your data keys to entry IDs "
|
|
30
|
+
"(default: $KEY_MAPPINGS)",
|
|
31
|
+
)
|
|
32
|
+
return parser.parse_args(argv)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main(argv: list[str] | None = None) -> None:
|
|
36
|
+
args = parse_args(argv)
|
|
37
|
+
load_dotenv()
|
|
38
|
+
|
|
39
|
+
file_path = args.file or os.getenv("FILE_PATH", "")
|
|
40
|
+
form_url = args.url or os.getenv("GOOGLE_FORM_URL", "")
|
|
41
|
+
mappings_path = args.key_mappings or os.getenv("KEY_MAPPINGS", "")
|
|
42
|
+
|
|
43
|
+
if not file_path:
|
|
44
|
+
print("Error: FILE_PATH is required")
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
if not form_url:
|
|
48
|
+
print("Error: GOOGLE_FORM_URL is required")
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
df = pd.read_json(file_path)
|
|
53
|
+
except (FileNotFoundError, ValueError) as e:
|
|
54
|
+
print(f"Error reading {file_path}: {e}")
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
|
|
57
|
+
key_mappings: dict[str, str] | None = None
|
|
58
|
+
if mappings_path:
|
|
59
|
+
with open(mappings_path) as f:
|
|
60
|
+
key_mappings = json.load(f)
|
|
61
|
+
|
|
62
|
+
submit_url = get_form_response_url(form_url)
|
|
63
|
+
print(f"Submit URL: {submit_url}")
|
|
64
|
+
|
|
65
|
+
results = post_data(submit_url, df, key_mappings)
|
|
66
|
+
for r in results:
|
|
67
|
+
status = r["status_code"]
|
|
68
|
+
payload = r["payload"]
|
|
69
|
+
if status >= 400:
|
|
70
|
+
print(f"Submission failed for row {r['index'] + 1}.")
|
|
71
|
+
print(f"Failed payload: {payload}")
|
|
72
|
+
print(f"Row {r['index'] + 1}: payload={payload}, status={status}")
|
|
73
|
+
|
|
74
|
+
print("Done!")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from post_to_google_form._submit import build_payload, post_data
|
|
11
|
+
from post_to_google_form._url import get_form_response_url
|
|
12
|
+
from post_to_google_form.cli import main, parse_args
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestBuildPayload:
|
|
16
|
+
def test_no_mappings_passes_row_through(self) -> None:
|
|
17
|
+
row = {"entry.1": "Mr", "entry.2": "Alice"}
|
|
18
|
+
result = build_payload(row)
|
|
19
|
+
assert result == {"entry.1": "Mr", "entry.2": "Alice"}
|
|
20
|
+
|
|
21
|
+
def test_with_mappings_renames_keys(self) -> None:
|
|
22
|
+
mappings = {"title": "entry.1", "name": "entry.2"}
|
|
23
|
+
row = {"title": "Mr", "name": "Alice", "extra": "ignored"}
|
|
24
|
+
result = build_payload(row, mappings)
|
|
25
|
+
assert result == {"entry.1": "Mr", "entry.2": "Alice"}
|
|
26
|
+
|
|
27
|
+
def test_unmapped_keys_are_omitted(self) -> None:
|
|
28
|
+
mappings = {"title": "entry.1"}
|
|
29
|
+
row = {"title": "Mr", "name": "Alice"}
|
|
30
|
+
result = build_payload(row, mappings)
|
|
31
|
+
assert result == {"entry.1": "Mr"}
|
|
32
|
+
|
|
33
|
+
def test_coerces_values_to_strings(self) -> None:
|
|
34
|
+
row = {"entry.1": 42, "entry.2": None}
|
|
35
|
+
result = build_payload(row)
|
|
36
|
+
assert result == {"entry.1": "42", "entry.2": "None"}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TestPostData:
|
|
40
|
+
@patch("post_to_google_form._submit.requests.post")
|
|
41
|
+
def test_direct_keys(self, mock_post: MagicMock) -> None:
|
|
42
|
+
mock_post.return_value.status_code = 200
|
|
43
|
+
data = [{"entry.1": "Mr"}, {"entry.1": "Ms"}]
|
|
44
|
+
results = post_data("https://example.com/formResponse", data)
|
|
45
|
+
assert len(results) == 2
|
|
46
|
+
assert results[0]["index"] == 0
|
|
47
|
+
assert results[0]["status_code"] == 200
|
|
48
|
+
assert results[0]["payload"] == {"entry.1": "Mr"}
|
|
49
|
+
|
|
50
|
+
@patch("post_to_google_form._submit.requests.post")
|
|
51
|
+
def test_with_mappings(self, mock_post: MagicMock) -> None:
|
|
52
|
+
mock_post.return_value.status_code = 200
|
|
53
|
+
key_mappings = {"title": "entry.1", "name": "entry.2"}
|
|
54
|
+
data = [{"title": "Mr", "name": "Alice"}, {"title": "Ms", "name": "Bob"}]
|
|
55
|
+
results = post_data("https://example.com/formResponse", data, key_mappings)
|
|
56
|
+
assert len(results) == 2
|
|
57
|
+
assert results[0]["payload"] == {"entry.1": "Mr", "entry.2": "Alice"}
|
|
58
|
+
assert results[1]["payload"] == {"entry.1": "Ms", "entry.2": "Bob"}
|
|
59
|
+
|
|
60
|
+
@patch("post_to_google_form._submit.requests.post")
|
|
61
|
+
def test_with_dataframe(self, mock_post: MagicMock) -> None:
|
|
62
|
+
mock_post.return_value.status_code = 200
|
|
63
|
+
df = pd.DataFrame([{"entry.1": "A"}])
|
|
64
|
+
results = post_data("https://example.com/formResponse", df)
|
|
65
|
+
assert len(results) == 1
|
|
66
|
+
|
|
67
|
+
@patch("post_to_google_form._submit.get_form_response_url")
|
|
68
|
+
@patch("post_to_google_form._submit.requests.post")
|
|
69
|
+
def test_resolves_url(
|
|
70
|
+
self, mock_post: MagicMock, mock_resolve: MagicMock
|
|
71
|
+
) -> None:
|
|
72
|
+
mock_resolve.return_value = "https://example.com/formResponse"
|
|
73
|
+
mock_post.return_value.status_code = 200
|
|
74
|
+
post_data("https://forms.gle/abc", [{"e.1": "v"}])
|
|
75
|
+
mock_resolve.assert_called_once_with("https://forms.gle/abc")
|
|
76
|
+
mock_post.assert_called_once_with(
|
|
77
|
+
"https://example.com/formResponse",
|
|
78
|
+
data={"e.1": "v"},
|
|
79
|
+
timeout=20,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class TestGetFormResponseUrl:
|
|
84
|
+
def test_already_form_response(self) -> None:
|
|
85
|
+
url = "https://docs.google.com/forms/d/e/ID/formResponse"
|
|
86
|
+
assert get_form_response_url(url) == url
|
|
87
|
+
|
|
88
|
+
def test_viewform_to_form_response(self) -> None:
|
|
89
|
+
url = "https://docs.google.com/forms/d/e/ID/viewform"
|
|
90
|
+
expected = "https://docs.google.com/forms/d/e/ID/formResponse"
|
|
91
|
+
assert get_form_response_url(url) == expected
|
|
92
|
+
|
|
93
|
+
def test_bare_path_appends_form_response(self) -> None:
|
|
94
|
+
url = "https://docs.google.com/forms/d/e/ID"
|
|
95
|
+
expected = "https://docs.google.com/forms/d/e/ID/formResponse"
|
|
96
|
+
assert get_form_response_url(url) == expected
|
|
97
|
+
|
|
98
|
+
@patch("requests.get")
|
|
99
|
+
def test_forms_gle_resolved(self, mock_get: MagicMock) -> None:
|
|
100
|
+
mock_get.return_value.url = "https://docs.google.com/forms/d/e/ID/viewform"
|
|
101
|
+
short = "https://forms.gle/abc123"
|
|
102
|
+
expected = "https://docs.google.com/forms/d/e/ID/formResponse"
|
|
103
|
+
assert get_form_response_url(short) == expected
|
|
104
|
+
mock_get.assert_called_once_with(short, allow_redirects=True, timeout=15)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TestParseArgs:
|
|
108
|
+
def test_defaults(self) -> None:
|
|
109
|
+
args = parse_args([])
|
|
110
|
+
assert args.file is None
|
|
111
|
+
assert args.url is None
|
|
112
|
+
assert args.key_mappings is None
|
|
113
|
+
|
|
114
|
+
def test_with_flags(self) -> None:
|
|
115
|
+
args = parse_args([
|
|
116
|
+
"--file", "data.json",
|
|
117
|
+
"--url", "https://example.com",
|
|
118
|
+
"--key-mappings", "mapping.json",
|
|
119
|
+
])
|
|
120
|
+
assert args.file == "data.json"
|
|
121
|
+
assert args.url == "https://example.com"
|
|
122
|
+
assert args.key_mappings == "mapping.json"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TestMain:
|
|
126
|
+
def test_missing_file_exits(self) -> None:
|
|
127
|
+
with pytest.raises(SystemExit):
|
|
128
|
+
main(["--url", "https://example.com/formResponse"])
|
|
129
|
+
|
|
130
|
+
@patch("post_to_google_form.cli.load_dotenv")
|
|
131
|
+
def test_missing_url_exits(
|
|
132
|
+
self, mock_load_dotenv: MagicMock, monkeypatch: pytest.MonkeyPatch
|
|
133
|
+
) -> None:
|
|
134
|
+
monkeypatch.setenv("GOOGLE_FORM_URL", "")
|
|
135
|
+
data_file = Path(__file__).parent / "test_data.json"
|
|
136
|
+
json.dump([{"entry.1": "Test"}], data_file.open("w"))
|
|
137
|
+
try:
|
|
138
|
+
with pytest.raises(SystemExit):
|
|
139
|
+
main(["--file", str(data_file)])
|
|
140
|
+
finally:
|
|
141
|
+
data_file.unlink(missing_ok=True)
|
|
142
|
+
|
|
143
|
+
@patch("post_to_google_form._submit.requests.post")
|
|
144
|
+
def test_submits_rows(self, mock_post: MagicMock) -> None:
|
|
145
|
+
mock_post.return_value.status_code = 200
|
|
146
|
+
rows = [{"entry.1": "Alice"}, {"entry.1": "Bob"}]
|
|
147
|
+
data_file = Path(__file__).parent / "test_data.json"
|
|
148
|
+
json.dump(rows, data_file.open("w"))
|
|
149
|
+
try:
|
|
150
|
+
main(["--file", str(data_file), "--url", "https://example.com/formResponse"])
|
|
151
|
+
finally:
|
|
152
|
+
data_file.unlink(missing_ok=True)
|
|
153
|
+
assert mock_post.call_count == 2
|
|
154
|
+
|
|
155
|
+
@patch("post_to_google_form._submit.requests.post")
|
|
156
|
+
def test_reports_failure(self, mock_post: MagicMock) -> None:
|
|
157
|
+
mock_post.return_value.status_code = 400
|
|
158
|
+
rows = [{"entry.1": "Carol"}]
|
|
159
|
+
data_file = Path(__file__).parent / "test_data.json"
|
|
160
|
+
json.dump(rows, data_file.open("w"))
|
|
161
|
+
try:
|
|
162
|
+
main(["--file", str(data_file), "--url", "https://example.com/formResponse"])
|
|
163
|
+
finally:
|
|
164
|
+
data_file.unlink(missing_ok=True)
|
|
165
|
+
mock_post.assert_called_once()
|
|
166
|
+
|
|
167
|
+
def test_read_error_exits(self) -> None:
|
|
168
|
+
with pytest.raises(SystemExit):
|
|
169
|
+
main(["--file", "/nonexistent/path.json", "--url", "https://example.com/formResponse"])
|