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.
- formelement-0.1.0/LICENSE +21 -0
- formelement-0.1.0/MANIFEST.in +3 -0
- formelement-0.1.0/PKG-INFO +94 -0
- formelement-0.1.0/README.md +79 -0
- formelement-0.1.0/pyproject.toml +36 -0
- formelement-0.1.0/setup.cfg +4 -0
- formelement-0.1.0/setup.py +18 -0
- formelement-0.1.0/src/formelement/__init__.py +15 -0
- formelement-0.1.0/src/formelement/answer_patterns.py +141 -0
- formelement-0.1.0/src/formelement/field_types.py +15 -0
- formelement-0.1.0/src/formelement/google_form_browser.py +152 -0
- formelement-0.1.0/src/formelement/google_form_submit.py +174 -0
- formelement-0.1.0/src/formelement.egg-info/PKG-INFO +94 -0
- formelement-0.1.0/src/formelement.egg-info/SOURCES.txt +15 -0
- formelement-0.1.0/src/formelement.egg-info/dependency_links.txt +1 -0
- formelement-0.1.0/src/formelement.egg-info/requires.txt +1 -0
- formelement-0.1.0/src/formelement.egg-info/top_level.txt +1 -0
|
@@ -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,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,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,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
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
selenium>=4.20
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
formelement
|