formelement 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Iqbal Rahman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include LICENSE
3
+ recursive-include src *.py
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.1
2
+ Name: formelement
3
+ Version: 0.1.0
4
+ Summary: Form automation utilities with browser drivers such as GoogleFormBrowser.
5
+ Home-page: https://pypi.org/project/formelement/
6
+ Author: Iqbal Rahman
7
+ Author-email: iqbal@example.com
8
+ License: MIT
9
+ Platform: UNKNOWN
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+
14
+ # formelement
15
+
16
+ `formelement` is a Python package for form automation.
17
+
18
+ It currently provides:
19
+
20
+ - answer pattern helpers
21
+ - Google Form browser automation with Selenium
22
+ - Google Form HTTP submission helpers
23
+
24
+ The package is structured so you can later extend it with other drivers such as `MonkeyFormBrowser`.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install formelement
30
+ ```
31
+
32
+ `selenium` is installed automatically because browser automation is part of the core package.
33
+
34
+ ## Quick start
35
+
36
+ Generate answer patterns:
37
+
38
+ ```python
39
+ from formelement import AnswerPatternGenerator
40
+
41
+ generator = AnswerPatternGenerator()
42
+ answers = generator.polaJawab3(0, 1, 5)
43
+ print(answers)
44
+ ```
45
+
46
+ Use the browser driver:
47
+
48
+ ```python
49
+ from selenium import webdriver
50
+ from formelement import AnswerFieldType, GoogleFormBrowser
51
+
52
+ driver = webdriver.Firefox()
53
+ form = GoogleFormBrowser(driver)
54
+
55
+ short_inputs = form.tombol(AnswerFieldType.ISIANPENDEK)
56
+ ```
57
+
58
+ Submit form answers:
59
+
60
+ ```python
61
+ from formelement import submit_answers
62
+
63
+ status, payload, encoded = submit_answers(
64
+ [["Alice", "24"], ["Yes"]],
65
+ data_module="my_form_data",
66
+ dry_run=True,
67
+ )
68
+ ```
69
+
70
+ Your `data_module` must define:
71
+
72
+ ```python
73
+ FORM_ID = "your-form-id"
74
+ FORM_ENTRY_IDS = [
75
+ ["entry.123", "entry.456"],
76
+ ["entry.789"],
77
+ ]
78
+ FORM_SESSION_STATE = ""
79
+ FORM_FBZX = ""
80
+ ```
81
+
82
+ ## Public API
83
+
84
+ - `AnswerPatternGenerator`
85
+ - `AnswerFieldType`
86
+ - `GoogleFormBrowser`
87
+ - `submit_answers`
88
+
89
+ Legacy aliases from the original project are still available:
90
+
91
+ - `dataGoogleForm`
92
+ - `dataPilihan`
93
+
94
+
@@ -0,0 +1,79 @@
1
+ # formelement
2
+
3
+ `formelement` is a Python package for form automation.
4
+
5
+ It currently provides:
6
+
7
+ - answer pattern helpers
8
+ - Google Form browser automation with Selenium
9
+ - Google Form HTTP submission helpers
10
+
11
+ The package is structured so you can later extend it with other drivers such as `MonkeyFormBrowser`.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install formelement
17
+ ```
18
+
19
+ `selenium` is installed automatically because browser automation is part of the core package.
20
+
21
+ ## Quick start
22
+
23
+ Generate answer patterns:
24
+
25
+ ```python
26
+ from formelement import AnswerPatternGenerator
27
+
28
+ generator = AnswerPatternGenerator()
29
+ answers = generator.polaJawab3(0, 1, 5)
30
+ print(answers)
31
+ ```
32
+
33
+ Use the browser driver:
34
+
35
+ ```python
36
+ from selenium import webdriver
37
+ from formelement import AnswerFieldType, GoogleFormBrowser
38
+
39
+ driver = webdriver.Firefox()
40
+ form = GoogleFormBrowser(driver)
41
+
42
+ short_inputs = form.tombol(AnswerFieldType.ISIANPENDEK)
43
+ ```
44
+
45
+ Submit form answers:
46
+
47
+ ```python
48
+ from formelement import submit_answers
49
+
50
+ status, payload, encoded = submit_answers(
51
+ [["Alice", "24"], ["Yes"]],
52
+ data_module="my_form_data",
53
+ dry_run=True,
54
+ )
55
+ ```
56
+
57
+ Your `data_module` must define:
58
+
59
+ ```python
60
+ FORM_ID = "your-form-id"
61
+ FORM_ENTRY_IDS = [
62
+ ["entry.123", "entry.456"],
63
+ ["entry.789"],
64
+ ]
65
+ FORM_SESSION_STATE = ""
66
+ FORM_FBZX = ""
67
+ ```
68
+
69
+ ## Public API
70
+
71
+ - `AnswerPatternGenerator`
72
+ - `AnswerFieldType`
73
+ - `GoogleFormBrowser`
74
+ - `submit_answers`
75
+
76
+ Legacy aliases from the original project are still available:
77
+
78
+ - `dataGoogleForm`
79
+ - `dataPilihan`
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "formelement"
7
+ version = "0.1.0"
8
+ description = "Form automation utilities with browser drivers such as GoogleFormBrowser."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Iqbal Rahman" }
14
+ ]
15
+ keywords = ["forms", "automation", "selenium", "google-forms"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ ]
27
+ dependencies = ["selenium>=4.20"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://pypi.org/project/formelement/"
31
+
32
+ [tool.setuptools]
33
+ package-dir = { "" = "src" }
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,18 @@
1
+ from setuptools import find_packages, setup
2
+
3
+
4
+ setup(
5
+ name="formelement",
6
+ version="0.1.0",
7
+ description="Form automation utilities with browser drivers such as GoogleFormBrowser.",
8
+ long_description=open("README.md", encoding="utf-8").read(),
9
+ long_description_content_type="text/markdown",
10
+ author="Iqbal Rahman",
11
+ author_email="iqbal@example.com",
12
+ license="MIT",
13
+ package_dir={"": "src"},
14
+ packages=find_packages(where="src"),
15
+ python_requires=">=3.9",
16
+ install_requires=["selenium>=4.20"],
17
+ url="https://pypi.org/project/formelement/",
18
+ )
@@ -0,0 +1,15 @@
1
+ """Public package interface for formelement."""
2
+
3
+ from .answer_patterns import AnswerPatternGenerator, dataGoogleForm
4
+ from .field_types import AnswerFieldType, dataPilihan
5
+ from .google_form_browser import GoogleFormBrowser
6
+ from .google_form_submit import submit_answers
7
+
8
+ __all__ = [
9
+ "AnswerFieldType",
10
+ "AnswerPatternGenerator",
11
+ "GoogleFormBrowser",
12
+ "dataGoogleForm",
13
+ "dataPilihan",
14
+ "submit_answers",
15
+ ]
@@ -0,0 +1,141 @@
1
+ import random
2
+
3
+
4
+ class AnswerPatternGenerator:
5
+ def hitungUsia(self, batasAwal: int, batasAkhir: int) -> int:
6
+ return random.randint(batasAwal, batasAkhir)
7
+
8
+ def pilihTipe(self, jenis: list[int]) -> int:
9
+ data: list[int] = []
10
+
11
+ for i in range(len(jenis)):
12
+ if jenis[i] != 0:
13
+ data.append(i)
14
+
15
+ return random.sample(data, 1)[0]
16
+
17
+ def pilihanCheckbox(self, awal: int, akhir: int, banyakData: int) -> list[int]:
18
+ temp = random.randint(1, banyakData)
19
+
20
+ listData: list[int] = []
21
+ i = awal
22
+ while i <= akhir:
23
+ listData.append(i)
24
+ i += 1
25
+
26
+ return random.sample(listData, temp)
27
+
28
+ def polaJawab1(
29
+ self,
30
+ pola: int,
31
+ awal: int,
32
+ banyakSoal: int,
33
+ kelipatan: int,
34
+ ) -> list[int]:
35
+ p = awal
36
+ hasil: list[int] = []
37
+ for _ in range(banyakSoal):
38
+ hasil.append(p)
39
+ p += kelipatan
40
+ return hasil
41
+
42
+ def polaJawab2(
43
+ self,
44
+ pola: int,
45
+ awal: int,
46
+ banyakSoal: int,
47
+ kelipatan: int,
48
+ ) -> list[int]:
49
+ p = awal
50
+ hasil: list[int] = []
51
+ for _ in range(banyakSoal):
52
+ if pola == 0:
53
+ s1 = random.choice([p, p + 1])
54
+ elif pola == 1:
55
+ s1 = random.choice([p, p + 1, p + 1])
56
+ elif pola == 2:
57
+ s1 = random.choice([p, p + 1, p + 1, p + 1])
58
+ elif pola == 3:
59
+ s1 = random.choice([p, p, p + 1])
60
+ elif pola == 4:
61
+ s1 = random.choice([p, p, p, p + 1])
62
+ else:
63
+ s1 = random.choice([p, p + 1])
64
+
65
+ hasil.append(s1)
66
+ return hasil
67
+
68
+ def polaJawab3(
69
+ self,
70
+ pola: int,
71
+ awal: int,
72
+ banyakSoal: int,
73
+ ) -> list[int]:
74
+ p = awal
75
+ hasil: list[int] = []
76
+ for _ in range(banyakSoal):
77
+ if pola == 0:
78
+ s1 = random.choice([p, p + 1, p + 2])
79
+ elif pola == 1:
80
+ s1 = random.choice([p, p + 1, p + 1, p + 2, p + 2])
81
+ elif pola == 2:
82
+ s1 = random.choice([p, p + 1, p + 1, p + 1, p + 2, p + 2, p + 2])
83
+ elif pola == 3:
84
+ s1 = random.choice(
85
+ [p, p + 1, p + 1, p + 1, p + 1, p + 2, p + 2, p + 2, p + 2]
86
+ )
87
+ elif pola == 4:
88
+ s1 = random.choice([p, p, p + 1, p + 1, p + 2])
89
+ elif pola == 5:
90
+ s1 = random.choice([p, p, p, p + 1, p + 1, p + 1, p + 2])
91
+ elif pola == 6:
92
+ s1 = random.choice([p, p, p, p, p + 1, p + 1, p + 1, p + 1, p + 2])
93
+ elif pola == 7:
94
+ s1 = random.choice([p, p, p + 1, p + 1, p + 1, p + 2])
95
+ else:
96
+ s1 = random.choice([p, p + 1, p + 2])
97
+
98
+ hasil.append(s1)
99
+ return hasil
100
+
101
+ def polaJawab4(
102
+ self,
103
+ pola: int,
104
+ awal: int,
105
+ banyakSoal: int,
106
+ kelipatan: int,
107
+ ) -> list[int]:
108
+ p = awal
109
+ hasil: list[int] = []
110
+ for _ in range(banyakSoal):
111
+ if pola == 0:
112
+ s1 = random.choice([p, p + 1, p + 2, p + 3])
113
+ elif pola == 1:
114
+ s1 = random.choice([p, p + 1, p + 1, p + 2, p + 2, p + 3, p + 3])
115
+ elif pola == 2:
116
+ s1 = random.choice(
117
+ [p, p + 1, p + 1, p + 1, p + 2, p + 2, p + 2, p + 3, p + 3, p + 3]
118
+ )
119
+ else:
120
+ s1 = random.choice([p, p + 1, p + 2, p + 3])
121
+
122
+ hasil.append(s1)
123
+ p += kelipatan
124
+ return hasil
125
+
126
+ def polaJawabCustom(
127
+ self,
128
+ awal: int,
129
+ soal: list[int],
130
+ kelipatan: int,
131
+ ) -> list[int]:
132
+ p = awal
133
+ hasil: list[int] = []
134
+ for i in range(len(soal)):
135
+ s1 = soal[i] + p
136
+ hasil.append(s1)
137
+ p += kelipatan
138
+ return hasil
139
+
140
+
141
+ dataGoogleForm = AnswerPatternGenerator
@@ -0,0 +1,15 @@
1
+ from enum import Enum
2
+
3
+
4
+ class AnswerFieldType(Enum):
5
+ ISIANPENDEK = 0
6
+ ISIANPANJANG = 1
7
+ KEBAWAH = 2
8
+ KESAMPING = 3
9
+ CHECKBOX = 4
10
+ LAINNYA = 5
11
+ DROPDOWN = 6
12
+ BUBBLE = 7
13
+
14
+
15
+ dataPilihan = AnswerFieldType
@@ -0,0 +1,152 @@
1
+ import random
2
+ import time
3
+
4
+ from selenium import webdriver
5
+ from selenium.webdriver.common.by import By
6
+ from selenium.webdriver.remote.webelement import WebElement
7
+
8
+ from .field_types import AnswerFieldType as pil
9
+
10
+
11
+ class GoogleFormBrowser:
12
+ driver: webdriver.Firefox
13
+
14
+ def __init__(self, driver: webdriver.Firefox):
15
+ self.driver = driver
16
+
17
+ def pindahHalaman(self):
18
+ time.sleep(3)
19
+ submit_button = self.driver.find_elements(By.XPATH, "//span[contains(text(), 'Berikutnya')]")
20
+ submit_button[0].click()
21
+
22
+ def kirim(self):
23
+ time.sleep(3)
24
+ submit_button = self.driver.find_elements(By.XPATH, "//span[contains(text(), 'Kirim')]")
25
+ submit_button[0].click()
26
+
27
+ def kirimJawaban(self):
28
+ time.sleep(2)
29
+ submit_button = self.driver.find_elements(By.XPATH, "//a[contains(text(), 'Kirim')]")
30
+ submit_button[0].click()
31
+
32
+ def tombol(self, pilihan: pil) -> list[WebElement]:
33
+ if pilihan == pil.ISIANPENDEK:
34
+ return self.driver.find_elements("css selector", ".whsOnd")
35
+ if pilihan == pil.ISIANPANJANG:
36
+ return self.driver.find_elements("css selector", ".KHxj8b")
37
+ if pilihan == pil.KEBAWAH:
38
+ return self.driver.find_elements("css selector", ".nWQGrd")
39
+ if pilihan == pil.KESAMPING:
40
+ return self.driver.find_elements("css selector", ".T5pZmf")
41
+ if pilihan == pil.CHECKBOX:
42
+ return self.driver.find_elements("css selector", ".uHMk6b")
43
+ if pilihan == pil.LAINNYA:
44
+ return self.driver.find_elements("css selector", ".Hvn9fb")
45
+ if pilihan == pil.DROPDOWN:
46
+ return self.driver.find_elements("css selector", ".ry3kXd")
47
+ if pilihan == pil.BUBBLE:
48
+ return self.driver.find_elements("css selector", ".AB7Lab")
49
+ return []
50
+
51
+ def pilihDropdown(self, pilihan: str):
52
+ time.sleep(2)
53
+ option = self.driver.find_elements(By.XPATH, f"//span[contains(text(), '{pilihan}')]")
54
+ option[len(option) - 1].click()
55
+
56
+ def hitungUsia(self, batasAwal: int, batasAkhir: int) -> int:
57
+ return random.randint(batasAwal, batasAkhir)
58
+
59
+ def pilihTipe(self, jenis: list[int]) -> int:
60
+ data: list[int] = []
61
+
62
+ for i in range(len(jenis)):
63
+ if jenis[i] != 0:
64
+ data.append(i)
65
+
66
+ return random.sample(data, 1)[0]
67
+
68
+ def pilihanCheckbox(self, awal: int, akhir: int, banyakData: int) -> list[int]:
69
+ temp = random.randint(1, banyakData)
70
+
71
+ listData: list[int] = []
72
+ i = awal
73
+ while i <= akhir:
74
+ listData.append(i)
75
+ i += 1
76
+
77
+ return random.sample(listData, temp)
78
+
79
+ def polaJawab1(self, pola: int, awal: int, banyakSoal: int, kelipatan: int, jawab: list[WebElement]):
80
+ p = awal
81
+ for _ in range(banyakSoal):
82
+ jawab[p].click()
83
+ p += kelipatan
84
+
85
+ def polaJawab2(self, pola: int, awal: int, banyakSoal: int, kelipatan: int, jawab: list[WebElement]):
86
+ p = awal
87
+ for _ in range(banyakSoal):
88
+ if pola == 0:
89
+ s1 = random.choice([p, p + 1])
90
+ elif pola == 1:
91
+ s1 = random.choice([p, p + 1, p + 1])
92
+ elif pola == 2:
93
+ s1 = random.choice([p, p + 1, p + 1, p + 1])
94
+ elif pola == 3:
95
+ s1 = random.choice([p, p, p + 1])
96
+ elif pola == 4:
97
+ s1 = random.choice([p, p, p, p + 1])
98
+ else:
99
+ s1 = random.choice([p, p + 1])
100
+
101
+ jawab[s1].click()
102
+ p += kelipatan
103
+
104
+ def polaJawab3(self, pola: int, awal: int, banyakSoal: int, kelipatan: int, jawab: list[WebElement]):
105
+ p = awal
106
+ for _ in range(banyakSoal):
107
+ if pola == 0:
108
+ s1 = random.choice([p, p + 1, p + 2])
109
+ elif pola == 1:
110
+ s1 = random.choice([p, p + 1, p + 1, p + 2, p + 2])
111
+ elif pola == 2:
112
+ s1 = random.choice([p, p + 1, p + 1, p + 1, p + 2, p + 2, p + 2])
113
+ elif pola == 3:
114
+ s1 = random.choice([p, p + 1, p + 1, p + 1, p + 1, p + 2, p + 2, p + 2, p + 2])
115
+ elif pola == 4:
116
+ s1 = random.choice([p, p, p + 1, p + 1, p + 2])
117
+ elif pola == 5:
118
+ s1 = random.choice([p, p, p, p + 1, p + 1, p + 1, p + 2])
119
+ elif pola == 6:
120
+ s1 = random.choice([p, p, p, p, p + 1, p + 1, p + 1, p + 1, p + 2])
121
+ elif pola == 7:
122
+ s1 = random.choice([p, p, p + 1, p + 1, p + 1, p + 2])
123
+ else:
124
+ s1 = random.choice([p, p + 1, p + 2])
125
+
126
+ jawab[s1].click()
127
+ p += kelipatan
128
+
129
+ def polaJawab4(self, pola: int, awal: int, banyakSoal: int, kelipatan: int, jawab: list[WebElement]):
130
+ p = awal
131
+ for _ in range(banyakSoal):
132
+ if pola == 0:
133
+ s1 = random.choice([p, p + 1, p + 2, p + 3])
134
+ elif pola == 1:
135
+ s1 = random.choice([p, p + 1, p + 1, p + 2, p + 2, p + 3, p + 3])
136
+ elif pola == 2:
137
+ s1 = random.choice([p, p + 1, p + 1, p + 1, p + 2, p + 2, p + 2, p + 3, p + 3, p + 3])
138
+ else:
139
+ s1 = random.choice([p, p + 1, p + 2, p + 3])
140
+
141
+ jawab[s1].click()
142
+ p += kelipatan
143
+
144
+ def polaJawabCustom(self, awal: int, soal: list[int], kelipatan: int, jawab: list[WebElement]):
145
+ p = awal
146
+ for i in range(len(soal)):
147
+ s1 = soal[i] + p
148
+ jawab[s1].click()
149
+ p += kelipatan
150
+
151
+
152
+ dataGoogleForm = GoogleFormBrowser
@@ -0,0 +1,174 @@
1
+ import json
2
+ import time
3
+ from importlib import import_module
4
+ from typing import Dict, Iterable, List, Optional, Sequence, Tuple
5
+ from urllib.error import HTTPError, URLError
6
+ from urllib.parse import urlencode
7
+ from urllib.request import Request, urlopen
8
+
9
+ POST_URL_TEMPLATE = "https://docs.google.com/forms/d/e/{form_id}/formResponse"
10
+
11
+
12
+ def _load_data_module(module_name: str):
13
+ return import_module(module_name)
14
+
15
+
16
+ def _ensure_pages_aligned(
17
+ entry_ids: Sequence[Sequence[str]],
18
+ answers: Sequence[Sequence[str]],
19
+ ) -> None:
20
+ if len(entry_ids) != len(answers):
21
+ raise ValueError(
22
+ "Answer pages do not match FORM_ENTRY_IDS pages. "
23
+ f"Expected {len(entry_ids)}, got {len(answers)}."
24
+ )
25
+
26
+ for index, (ids, values) in enumerate(zip(entry_ids, answers)):
27
+ if len(ids) != len(values):
28
+ raise ValueError(
29
+ f"Answer count on page {index + 1} mismatches FORM_ENTRY_IDS "
30
+ f"(expected {len(ids)}, got {len(values)})."
31
+ )
32
+
33
+
34
+ def _compute_page_history(total_pages: int) -> str:
35
+ if total_pages <= 0:
36
+ return "0"
37
+ return ",".join(str(i) for i in range(total_pages))
38
+
39
+
40
+ def _extract_entry_token(entry_id: str) -> int:
41
+ try:
42
+ return int(entry_id.split(".", 1)[1])
43
+ except (IndexError, ValueError) as error:
44
+ raise ValueError(f"Invalid entry id format: {entry_id}") from error
45
+
46
+
47
+ def _build_partial_response(
48
+ entry_pages: Sequence[Sequence[str]],
49
+ answer_pages: Sequence[Sequence[str]],
50
+ session_state: str,
51
+ ) -> Optional[str]:
52
+ entries: List[List[object]] = []
53
+ for ids, values in zip(entry_pages, answer_pages):
54
+ for entry_id, answer in zip(ids, values):
55
+ entries.append(
56
+ [
57
+ None,
58
+ _extract_entry_token(entry_id),
59
+ [answer],
60
+ 0,
61
+ ]
62
+ )
63
+
64
+ if not entries:
65
+ return None
66
+
67
+ payload: List[object] = [entries, None]
68
+ if session_state:
69
+ payload.append(session_state)
70
+
71
+ return json.dumps(payload, separators=(",", ":"))
72
+
73
+
74
+ def _build_payload(
75
+ entry_ids: Sequence[Sequence[str]],
76
+ answers: Sequence[Sequence[str]],
77
+ session_state: str,
78
+ fbzx: Optional[str] = None,
79
+ ) -> Dict[str, str]:
80
+ total_pages = len(entry_ids)
81
+ last_page_ids = entry_ids[-1] if entry_ids else []
82
+ last_page_answers = answers[-1] if answers else []
83
+ timestamp_ms = str(int(time.time() * 1000))
84
+
85
+ payload: Dict[str, str] = {
86
+ entry_id: answer
87
+ for entry_id, answer in zip(last_page_ids, last_page_answers)
88
+ }
89
+
90
+ for entry_id in last_page_ids:
91
+ payload[f"{entry_id}_sentinel"] = ""
92
+
93
+ payload["dlut"] = timestamp_ms
94
+
95
+ partial = _build_partial_response(
96
+ entry_ids[:-1],
97
+ answers[:-1],
98
+ session_state,
99
+ )
100
+ if partial is not None:
101
+ payload["partialResponse"] = partial
102
+
103
+ payload["pageHistory"] = _compute_page_history(total_pages)
104
+ payload["fvv"] = "1"
105
+ if fbzx:
106
+ payload["fbzx"] = fbzx
107
+ payload["submissionTimestamp"] = timestamp_ms
108
+
109
+ return payload
110
+
111
+
112
+ def submit_answers(
113
+ answers: Iterable[Iterable[str]],
114
+ *,
115
+ data_module: str = "2_data",
116
+ session_state: Optional[str] = None,
117
+ fbzx: Optional[str] = None,
118
+ dry_run: bool = False,
119
+ ) -> Tuple[Optional[int], Dict[str, str], str]:
120
+ """
121
+ Submit a multi-page Google Form response.
122
+
123
+ The referenced data module must expose:
124
+ - FORM_ID
125
+ - FORM_ENTRY_IDS
126
+ - optionally FORM_SESSION_STATE
127
+ - optionally FORM_FBZX
128
+ """
129
+
130
+ normalized_answers = [
131
+ [str(value) for value in page]
132
+ for page in answers
133
+ ]
134
+
135
+ module = _load_data_module(data_module)
136
+ form_id: str = getattr(module, "FORM_ID", "")
137
+ entry_ids: Sequence[Sequence[str]] = getattr(module, "FORM_ENTRY_IDS", [])
138
+ default_session_state: str = getattr(module, "FORM_SESSION_STATE", "")
139
+ default_fbzx: str = getattr(module, "FORM_FBZX", "")
140
+
141
+ if not form_id:
142
+ raise ValueError("FORM_ID is missing in the data module.")
143
+
144
+ entry_ids = [list(page) for page in entry_ids]
145
+ _ensure_pages_aligned(entry_ids, normalized_answers)
146
+
147
+ payload = _build_payload(
148
+ entry_ids,
149
+ normalized_answers,
150
+ session_state if session_state is not None else default_session_state,
151
+ fbzx if fbzx is not None else default_fbzx,
152
+ )
153
+
154
+ encoded = urlencode(payload)
155
+
156
+ if dry_run:
157
+ return None, payload, encoded
158
+
159
+ request = Request(
160
+ POST_URL_TEMPLATE.format(form_id=form_id),
161
+ data=encoded.encode("utf-8"),
162
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
163
+ )
164
+
165
+ try:
166
+ with urlopen(request) as response:
167
+ response.read()
168
+ status = response.status
169
+ except HTTPError as error:
170
+ raise RuntimeError(f"HTTP {error.code}: {error.reason}") from error
171
+ except URLError as error:
172
+ raise RuntimeError(f"Request failed: {error.reason}") from error
173
+
174
+ return status, payload, encoded
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.1
2
+ Name: formelement
3
+ Version: 0.1.0
4
+ Summary: Form automation utilities with browser drivers such as GoogleFormBrowser.
5
+ Home-page: https://pypi.org/project/formelement/
6
+ Author: Iqbal Rahman
7
+ Author-email: iqbal@example.com
8
+ License: MIT
9
+ Platform: UNKNOWN
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+
14
+ # formelement
15
+
16
+ `formelement` is a Python package for form automation.
17
+
18
+ It currently provides:
19
+
20
+ - answer pattern helpers
21
+ - Google Form browser automation with Selenium
22
+ - Google Form HTTP submission helpers
23
+
24
+ The package is structured so you can later extend it with other drivers such as `MonkeyFormBrowser`.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install formelement
30
+ ```
31
+
32
+ `selenium` is installed automatically because browser automation is part of the core package.
33
+
34
+ ## Quick start
35
+
36
+ Generate answer patterns:
37
+
38
+ ```python
39
+ from formelement import AnswerPatternGenerator
40
+
41
+ generator = AnswerPatternGenerator()
42
+ answers = generator.polaJawab3(0, 1, 5)
43
+ print(answers)
44
+ ```
45
+
46
+ Use the browser driver:
47
+
48
+ ```python
49
+ from selenium import webdriver
50
+ from formelement import AnswerFieldType, GoogleFormBrowser
51
+
52
+ driver = webdriver.Firefox()
53
+ form = GoogleFormBrowser(driver)
54
+
55
+ short_inputs = form.tombol(AnswerFieldType.ISIANPENDEK)
56
+ ```
57
+
58
+ Submit form answers:
59
+
60
+ ```python
61
+ from formelement import submit_answers
62
+
63
+ status, payload, encoded = submit_answers(
64
+ [["Alice", "24"], ["Yes"]],
65
+ data_module="my_form_data",
66
+ dry_run=True,
67
+ )
68
+ ```
69
+
70
+ Your `data_module` must define:
71
+
72
+ ```python
73
+ FORM_ID = "your-form-id"
74
+ FORM_ENTRY_IDS = [
75
+ ["entry.123", "entry.456"],
76
+ ["entry.789"],
77
+ ]
78
+ FORM_SESSION_STATE = ""
79
+ FORM_FBZX = ""
80
+ ```
81
+
82
+ ## Public API
83
+
84
+ - `AnswerPatternGenerator`
85
+ - `AnswerFieldType`
86
+ - `GoogleFormBrowser`
87
+ - `submit_answers`
88
+
89
+ Legacy aliases from the original project are still available:
90
+
91
+ - `dataGoogleForm`
92
+ - `dataPilihan`
93
+
94
+
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ setup.py
6
+ src/formelement/__init__.py
7
+ src/formelement/answer_patterns.py
8
+ src/formelement/field_types.py
9
+ src/formelement/google_form_browser.py
10
+ src/formelement/google_form_submit.py
11
+ src/formelement.egg-info/PKG-INFO
12
+ src/formelement.egg-info/SOURCES.txt
13
+ src/formelement.egg-info/dependency_links.txt
14
+ src/formelement.egg-info/requires.txt
15
+ src/formelement.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ selenium>=4.20
@@ -0,0 +1 @@
1
+ formelement