sysu-check-in 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 Xiao Li
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,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: sysu-check-in
3
+ Version: 0.1.0
4
+ Summary: Command-line client for SYSU face-recognition check-in
5
+ Keywords: sysu,check-in,face-check-in,cli
6
+ Author: Xiao Li
7
+ Author-email: Xiao Li <xiaoli3397@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Utilities
20
+ Requires-Dist: requests>=2.34.2
21
+ Requires-Dist: pycryptodome>=3.23.0
22
+ Requires-Python: >=3.10
23
+ Project-URL: Homepage, https://github.com/undefined443/sysu-check-in
24
+ Project-URL: Issues, https://github.com/undefined443/sysu-check-in/issues
25
+ Project-URL: Repository, https://github.com/undefined443/sysu-check-in
26
+ Description-Content-Type: text/markdown
27
+
28
+ # sysu-check-in
29
+
30
+ [![CI](https://github.com/undefined443/sysu-check-in/actions/workflows/ci.yml/badge.svg)](https://github.com/undefined443/sysu-check-in/actions/workflows/ci.yml)
31
+
32
+ A command-line client for completing SYSU check-in activities.
33
+
34
+ ## Installation
35
+
36
+ Install the latest release from PyPI:
37
+
38
+ ```bash
39
+ uv tool install sysu-check-in
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ Save the JPEG face image to submit as `face.jpg`, then run:
45
+
46
+ ```bash
47
+ sysu-check-in <student-id>
48
+ ```
49
+
50
+ You can also supply a different image path:
51
+
52
+ ```bash
53
+ sysu-check-in <student-id> --image <image-path>
54
+ ```
55
+
56
+ The command prints the raw response from the last required submission. It exits with a non-zero status and an error message if the image is missing, the request fails, an activity is unavailable, or the service rejects a submission.
57
+
58
+ ## Development
59
+
60
+ Install the Git hooks once:
61
+
62
+ ```bash
63
+ uv run --group dev pre-commit install
64
+ ```
65
+
66
+ Run the test suite and static checks with:
67
+
68
+ ```bash
69
+ uv run --group dev pytest
70
+ uv run --group dev ruff check .
71
+ uv run --group dev pyright
72
+ ```
73
+
74
+ ## License
75
+
76
+ Distributed under the [MIT License](LICENSE).
@@ -0,0 +1,49 @@
1
+ # sysu-check-in
2
+
3
+ [![CI](https://github.com/undefined443/sysu-check-in/actions/workflows/ci.yml/badge.svg)](https://github.com/undefined443/sysu-check-in/actions/workflows/ci.yml)
4
+
5
+ A command-line client for completing SYSU check-in activities.
6
+
7
+ ## Installation
8
+
9
+ Install the latest release from PyPI:
10
+
11
+ ```bash
12
+ uv tool install sysu-check-in
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ Save the JPEG face image to submit as `face.jpg`, then run:
18
+
19
+ ```bash
20
+ sysu-check-in <student-id>
21
+ ```
22
+
23
+ You can also supply a different image path:
24
+
25
+ ```bash
26
+ sysu-check-in <student-id> --image <image-path>
27
+ ```
28
+
29
+ The command prints the raw response from the last required submission. It exits with a non-zero status and an error message if the image is missing, the request fails, an activity is unavailable, or the service rejects a submission.
30
+
31
+ ## Development
32
+
33
+ Install the Git hooks once:
34
+
35
+ ```bash
36
+ uv run --group dev pre-commit install
37
+ ```
38
+
39
+ Run the test suite and static checks with:
40
+
41
+ ```bash
42
+ uv run --group dev pytest
43
+ uv run --group dev ruff check .
44
+ uv run --group dev pyright
45
+ ```
46
+
47
+ ## License
48
+
49
+ Distributed under the [MIT License](LICENSE).
@@ -0,0 +1,79 @@
1
+ [project]
2
+ name = "sysu-check-in"
3
+ version = "0.1.0"
4
+ description = "Command-line client for SYSU face-recognition check-in"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ keywords = [
9
+ "sysu",
10
+ "check-in",
11
+ "face-check-in",
12
+ "cli",
13
+ ]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: Console",
17
+ "Intended Audience :: End Users/Desktop",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Utilities",
25
+ ]
26
+ requires-python = ">=3.10"
27
+ dependencies = [
28
+ "requests>=2.34.2",
29
+ "pycryptodome>=3.23.0",
30
+ ]
31
+
32
+ [[project.authors]]
33
+ name = "Xiao Li"
34
+ email = "xiaoli3397@gmail.com"
35
+
36
+ [project.scripts]
37
+ sysu-check-in = "sysu_check_in.cli:main"
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/undefined443/sysu-check-in"
41
+ Issues = "https://github.com/undefined443/sysu-check-in/issues"
42
+ Repository = "https://github.com/undefined443/sysu-check-in"
43
+
44
+ [build-system]
45
+ requires = ["uv_build>=0.12.19,<0.13.0"]
46
+ build-backend = "uv_build"
47
+
48
+ [dependency-groups]
49
+ dev = [
50
+ "pyright>=1.1.400",
51
+ "pre-commit>=4.0",
52
+ "pytest>=8.3",
53
+ "ruff>=0.11",
54
+ "twine>=7.0.0",
55
+ ]
56
+
57
+ [tool.ruff]
58
+ line-length = 88
59
+ target-version = "py310"
60
+
61
+ [tool.ruff.lint]
62
+ select = [
63
+ "E",
64
+ "F",
65
+ "I",
66
+ ]
67
+
68
+ [tool.pyright]
69
+ include = [
70
+ "src",
71
+ "tests",
72
+ ]
73
+ pythonVersion = "3.10"
74
+ typeCheckingMode = "standard"
75
+
76
+ [[tool.uv.index]]
77
+ name = "pypi"
78
+ url = "https://pypi.org/simple"
79
+ default = true
@@ -0,0 +1,66 @@
1
+ [project]
2
+ name = "sysu-check-in"
3
+ version = "0.1.0"
4
+ description = "Command-line client for SYSU face-recognition check-in"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ keywords = ["sysu", "check-in", "face-check-in", "cli"]
9
+ classifiers = [
10
+ "Development Status :: 4 - Beta",
11
+ "Environment :: Console",
12
+ "Intended Audience :: End Users/Desktop",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3 :: Only",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Topic :: Utilities",
20
+ ]
21
+ authors = [
22
+ { name = "Xiao Li", email = "xiaoli3397@gmail.com" }
23
+ ]
24
+ requires-python = ">=3.10"
25
+ dependencies = [
26
+ "requests>=2.34.2",
27
+ "pycryptodome>=3.23.0",
28
+ ]
29
+
30
+ [project.scripts]
31
+ sysu-check-in = "sysu_check_in.cli:main"
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/undefined443/sysu-check-in"
35
+ Issues = "https://github.com/undefined443/sysu-check-in/issues"
36
+ Repository = "https://github.com/undefined443/sysu-check-in"
37
+
38
+ [build-system]
39
+ requires = ["uv_build>=0.12.19,<0.13.0"]
40
+ build-backend = "uv_build"
41
+
42
+ [dependency-groups]
43
+ dev = [
44
+ "pyright>=1.1.400",
45
+ "pre-commit>=4.0",
46
+ "pytest>=8.3",
47
+ "ruff>=0.11",
48
+ "twine>=7.0.0",
49
+ ]
50
+
51
+ [tool.ruff]
52
+ line-length = 88
53
+ target-version = "py310"
54
+
55
+ [tool.ruff.lint]
56
+ select = ["E", "F", "I"]
57
+
58
+ [tool.pyright]
59
+ include = ["src", "tests"]
60
+ pythonVersion = "3.10"
61
+ typeCheckingMode = "standard"
62
+
63
+ [[tool.uv.index]]
64
+ name = "pypi"
65
+ url = "https://pypi.org/simple"
66
+ default = true
@@ -0,0 +1,15 @@
1
+ """SYSU face-recognition check-in package."""
2
+
3
+ from sysu_check_in.core import (
4
+ DEFAULT_IMAGE_PATH,
5
+ CheckInRejectedError,
6
+ NoActiveActivityError,
7
+ check_in,
8
+ )
9
+
10
+ __all__ = [
11
+ "DEFAULT_IMAGE_PATH",
12
+ "CheckInRejectedError",
13
+ "NoActiveActivityError",
14
+ "check_in",
15
+ ]
@@ -0,0 +1,5 @@
1
+ """Run the sysu-check-in command as a module."""
2
+
3
+ from sysu_check_in.cli import main
4
+
5
+ main()
@@ -0,0 +1,49 @@
1
+ """Command-line interface for sysu_check_in."""
2
+
3
+ import argparse
4
+ from importlib.metadata import version
5
+
6
+ import requests
7
+
8
+ from sysu_check_in import core
9
+
10
+
11
+ def _parse_args() -> argparse.Namespace:
12
+ """Parse command-line arguments.
13
+
14
+ Returns:
15
+ Parsed command-line arguments.
16
+ """
17
+ parser = argparse.ArgumentParser(
18
+ prog="sysu-check-in",
19
+ description="Submit a face image to the SYSU check-in service.",
20
+ )
21
+ parser.add_argument(
22
+ "--version",
23
+ action="version",
24
+ version=f"%(prog)s {version('sysu-check-in')}",
25
+ )
26
+ parser.add_argument("student_id", help="Student ID used for check-in.")
27
+ parser.add_argument(
28
+ "--image",
29
+ default=core.DEFAULT_IMAGE_PATH,
30
+ metavar="PATH",
31
+ help=f"Path to the JPEG face image (default: {core.DEFAULT_IMAGE_PATH}).",
32
+ )
33
+ return parser.parse_args()
34
+
35
+
36
+ def main() -> None:
37
+ """Run the command-line interface."""
38
+ args = _parse_args()
39
+ try:
40
+ print(core.check_in(args.student_id, args.image))
41
+ except (
42
+ FileNotFoundError,
43
+ core.CheckInRejectedError,
44
+ core.NoActiveActivityError,
45
+ requests.RequestException,
46
+ ValueError,
47
+ KeyError,
48
+ ) as error:
49
+ raise SystemExit(f"Check-in failed: {error}") from error
@@ -0,0 +1,203 @@
1
+ """Client for the SYSU face-recognition check-in service."""
2
+
3
+ import base64
4
+ import time
5
+ from dataclasses import dataclass
6
+
7
+ import requests
8
+ from Crypto.Cipher import AES
9
+ from Crypto.Util.Padding import pad
10
+
11
+ # AES-128 key shared by every request; the hex decodes to b"sh12345678901234".
12
+ _AES_KEY = bytes.fromhex("73683132333435363738393031323334")
13
+
14
+ _BASE_URL = "https://facerecog.sysu.edu.cn/sign"
15
+ _ACTIVE_SIGN_STATUS = 2
16
+
17
+ DEFAULT_IMAGE_PATH = "face.jpg"
18
+
19
+ # Campus coordinates used for position submission.
20
+ _CAMPUS_LONGITUDE = "113.9539"
21
+ _CAMPUS_LATITUDE = "22.801604"
22
+
23
+ _HEADERS = {
24
+ "User-Agent": (
25
+ "Mozilla/5.0 (Linux; Android 16; FLC-AN00 Build/HONORFLC-AN00; wv) "
26
+ "AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 "
27
+ "Chrome/138.0.7204.180 Mobile Safari/537.36 XWEB/1380347 "
28
+ "MMWEBSDK/20250202 MMWEBID/6351 wxwork/5.0.6.66174 "
29
+ "MicroMessenger/8.0.28.48(0x28001c30) MiniProgramEnv/android "
30
+ "Luggage/3.0.2.95ef3f83 NetType/WIFI Language/en ABI/arm64"
31
+ ),
32
+ "Accept-Encoding": "gzip,compress,br,deflate",
33
+ "charset": "utf-8",
34
+ }
35
+
36
+
37
+ class NoActiveActivityError(Exception):
38
+ """Raised when the service has no active check-in activity."""
39
+
40
+
41
+ class CheckInRejectedError(Exception):
42
+ """Raised when the service rejects a check-in submission."""
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class _Activity:
47
+ """Details needed to submit an active check-in activity."""
48
+
49
+ activity_id: str
50
+ collects_gps: bool
51
+ requires_face: bool
52
+
53
+
54
+ def _timestamp() -> int:
55
+ """Return the current Unix timestamp in seconds."""
56
+ return int(time.time())
57
+
58
+
59
+ def _timestamp_milliseconds() -> int:
60
+ """Return the current Unix timestamp in milliseconds."""
61
+ return int(time.time() * 1000)
62
+
63
+
64
+ def _encrypt(plaintext: str) -> str:
65
+ """Encrypt a payload with AES-128-ECB.
66
+
67
+ Args:
68
+ plaintext: Plain-text payload to encrypt.
69
+
70
+ Returns:
71
+ The encrypted payload encoded as a hexadecimal string.
72
+ """
73
+ cipher = AES.new(_AES_KEY, AES.MODE_ECB)
74
+ return cipher.encrypt(pad(plaintext.encode(), AES.block_size)).hex()
75
+
76
+
77
+ def _read_image_base64(image_path: str) -> str:
78
+ """Read an image file and encode its content as Base64.
79
+
80
+ Args:
81
+ image_path: Path to the image file.
82
+
83
+ Returns:
84
+ The Base64-encoded image content.
85
+ """
86
+ with open(image_path, "rb") as file:
87
+ return base64.b64encode(file.read()).decode("utf-8")
88
+
89
+
90
+ def _post(endpoint_path: str, form_data: dict[str, str]) -> requests.Response:
91
+ """Send form data to a check-in endpoint.
92
+
93
+ Args:
94
+ endpoint_path: Endpoint path relative to the service base URL.
95
+ form_data: Form data to send in the request body.
96
+
97
+ Returns:
98
+ The successful HTTP response.
99
+ """
100
+ response = requests.post(
101
+ f"{_BASE_URL}/{endpoint_path}",
102
+ data=form_data,
103
+ headers=_HEADERS,
104
+ timeout=15,
105
+ )
106
+ response.raise_for_status()
107
+ return response
108
+
109
+
110
+ def _raise_for_rejection(response: requests.Response) -> None:
111
+ """Raise an error when the service reports a rejected submission.
112
+
113
+ Args:
114
+ response: HTTP response returned by the check-in service.
115
+
116
+ Raises:
117
+ CheckInRejectedError: If the service response has a non-success code.
118
+ """
119
+ payload = response.json()
120
+ if payload["code"] != 1:
121
+ raise CheckInRejectedError(payload["msg"])
122
+
123
+
124
+ def _get_active_activity(student_id: str) -> _Activity:
125
+ """Fetch details for the currently active check-in activity.
126
+
127
+ Args:
128
+ student_id: Student ID used for check-in.
129
+
130
+ Returns:
131
+ Details of the active check-in activity.
132
+ """
133
+ response = _post(
134
+ "getActivityList", {"sKey": _encrypt(f"{student_id}##{_timestamp()}")}
135
+ )
136
+ activities = response.json()["data"]["rows"]
137
+ for activity in activities:
138
+ if activity["iSignStatus"] == _ACTIVE_SIGN_STATUS:
139
+ return _Activity(
140
+ activity_id=activity["sActId"],
141
+ collects_gps=activity["iCollectGPS"] == 1,
142
+ requires_face=activity["isNeedFace"] == 1,
143
+ )
144
+ raise NoActiveActivityError("No active check-in activity is available.")
145
+
146
+
147
+ def _submit_gps(student_id: str, activity_id: str) -> str:
148
+ """Submit the campus position for an activity.
149
+
150
+ Args:
151
+ student_id: Student ID used for check-in.
152
+ activity_id: ID of the active check-in activity.
153
+
154
+ Returns:
155
+ The raw response returned by the check-in service.
156
+ """
157
+ plaintext = (
158
+ f"{student_id}##{activity_id}####{_CAMPUS_LONGITUDE}##"
159
+ f"{_CAMPUS_LATITUDE}##{_timestamp_milliseconds()}"
160
+ )
161
+ response = _post("submitPosition", {"sKey": _encrypt(plaintext)})
162
+ _raise_for_rejection(response)
163
+ return response.text
164
+
165
+
166
+ def _submit_face(student_id: str, activity_id: str, image_path: str) -> str:
167
+ """Submit a face image to complete the check-in.
168
+
169
+ Args:
170
+ student_id: Student ID used for check-in.
171
+ activity_id: ID of the active check-in activity.
172
+ image_path: Path to the JPEG face image.
173
+
174
+ Returns:
175
+ The raw response returned by the check-in service.
176
+ """
177
+ face_image_base64 = _read_image_base64(image_path)
178
+ form_data = {
179
+ "sKey": _encrypt(f"{student_id}##{activity_id}##{_timestamp()}"),
180
+ "sImage": f"data:image/jpeg;base64,{face_image_base64}",
181
+ }
182
+ response = _post("submitSignFace", form_data)
183
+ _raise_for_rejection(response)
184
+ return response.text
185
+
186
+
187
+ def check_in(student_id: str, image_path: str = DEFAULT_IMAGE_PATH) -> str:
188
+ """Perform all required check-in submissions for a student ID.
189
+
190
+ Args:
191
+ student_id: Student ID used for check-in.
192
+ image_path: Path to the JPEG face image.
193
+
194
+ Returns:
195
+ The raw response returned by the check-in service.
196
+ """
197
+ activity = _get_active_activity(student_id)
198
+ response = ""
199
+ if activity.collects_gps:
200
+ response = _submit_gps(student_id, activity.activity_id)
201
+ if activity.requires_face:
202
+ response = _submit_face(student_id, activity.activity_id, image_path)
203
+ return response