├── .editorconfig ├── .gitbook.yaml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── README.md ├── SUMMARY.md ├── apis │ ├── README.md │ ├── card.md │ ├── course.md │ ├── create.md │ ├── exam.md │ ├── person.md │ └── public.md ├── changelog.md ├── install.md └── quickstart.md ├── hdu_api ├── __init__.py ├── __version__.py ├── _internal_utils.py ├── _pyDes.py ├── api.py ├── config.py ├── exceptions.py ├── models.py └── sessions.py ├── setup.cfg └── setup.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.py] 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [*.html] 14 | indent_size = 2 15 | 16 | [*.json] 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.gitbook.yaml: -------------------------------------------------------------------------------- 1 | root: ./docs/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # idea 107 | .idea 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 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. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE pytest.ini requirements.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
4 |5 | A simple SDK for HDU. 6 |
7 | 14 | 15 | --- 16 | 17 | hdu-api 是一个集结 HDU 所有教务管理服务的 SDK,提供了一卡通服务、考试、课表、选课和一些公共信息如空闲教室、上课时间等信息的 API。 hdu-api 主要基于 Requests 库和 Beautiful Soup 库写成。 18 | 19 | ## 特性 20 | 21 | * 支持一卡通服务的信息查询 22 | * 支持教务管理系统的考试、课程等信息查询 23 | * 支持学生管理系统的信息查询 24 | * 支持 ihdu PC 版和手机版的信息查询 25 | * 易用,友好的 API 26 | * 基于 requests 库,支持每个网站的 session 使用和管理,重用性高 27 | * 自定义,对返回数据进行自定义化 28 | 29 | ## 安装 30 | 31 | 使用包管理器安装,如 pip: 32 | 33 | ```text 34 | pip install hdu-api 35 | ``` 36 | 37 | ## 快速开始 38 | 39 | ```text 40 | >>> import hdu_api 41 | >>> hdu = hdu_api.HDU('学号', '密码') 42 | >>> client = hdu.create() 43 | >>> client.exam.schedule_current() 44 | [{'classroom': '第12教研楼201', 45 | 'course_name': '操作系统(甲)', 46 | 'exam_time': '2019年1月17日(09:00-11:00)', 47 | 'exam_type': '', 48 | 'seat': '10', 49 | 'select_code': '(2018-2019-1)-A0507050-06018-1', 50 | 'staff_name': 'xxx'}, 51 | 52 | ... 53 | 54 | {'classroom': '第6教研楼北308', 55 | 'course_name': '软件工程(甲)', 56 | 'exam_time': '2019年1月9日(13:45-15:45)', 57 | 'exam_type': '', 58 | 'seat': '24', 59 | 'select_code': '(2018-2019-1)-A0507190-06061-2', 60 | 'staff_name': 'xxx'}] 61 | 62 | >>> client.card.balance() 63 | [{'account_id': 'xxxxxx', 64 | 'balance': '69.97', 65 | 'card_id': 'xxxxxx', 66 | 'staff_id': 'xxxxxx', 67 | 'staff_name': 'xxx'}] 68 | ``` 69 | 70 | ## 文档 71 | 72 | https://liuxingran.gitbook.io/hdu-api/ 73 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | > hdu-api: A simple SDK for HDU. 4 | 5 |    6 | 7 | ```text 8 | ___ ___ ___ ___ 9 | / /\ / /\ / /\ / /\ ___ ___ 10 | / /:/ / /::\ / /:/ / /::\ / /\ /__/\ 11 | / /:/ / /:/\:\ / /:/ / /:/\:\ / /::\ \__\:\ 12 | / /::\ ___ / /:/ \:\ / /:/ / /::\ \:\ / /:/\:\ / /::\ 13 | /__/:/\:\ /\/__/:/ \__\:|/__/:/ /\/__/:/\:\_\:\ / /::\ \:\ __/ /:/\/ 14 | \__\/ \:\/:/\ \:\ / /:/\ \:\ /:/\__\/ \:\/://__/:/\:\_\:\/__/\/:/~~ 15 | \__\::/ \ \:\ /:/ \ \:\ /:/ \__\::/ \__\/ \:\/:/\ \::/ 16 | / /:/ \ \:\/:/ \ \:\/:/ / /:/ \ \::/ \ \:\ 17 | /__/:/ \__\::/ \ \::/ /__/:/ \__\/ \__\/ 18 | \__\/ ~~ \__\/ \__\/ 19 | ``` 20 | 21 | hdu-api 是一个集结 HDU 所有教务管理服务的 SDK,提供了一卡通服务、考试、课表、选课和一些公共信息如空闲教室、上课时间等信息的 API。 hdu-api 主要基于 Requests 库和 Beautiful Soup 库写成。 22 | 23 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | * [简介](README.md) 4 | * [安装](install.md) 5 | * [快速上手](quickstart.md) 6 | * [APIS](apis/README.md) 7 | * [hdu\_api](apis/create.md) 8 | * [一卡通 API](apis/card.md) 9 | * [考试 API](apis/exam.md) 10 | * [课程 API](apis/course.md) 11 | * [个人信息 API](apis/person.md) 12 | * [公共信息 API](apis/public.md) 13 | * [changelog](changelog.md) 14 | 15 | -------------------------------------------------------------------------------- /docs/apis/README.md: -------------------------------------------------------------------------------- 1 | # APIS 2 | 3 | `hdu-api` 提供了非常丰富的 API,此文档包含了全部 API 的详细描述,请选择你感兴趣的部分开始了解它们吧。 4 | 5 | {% page-ref page="create.md" %} 6 | 7 | {% page-ref page="card.md" %} 8 | 9 | {% page-ref page="exam.md" %} 10 | 11 | {% page-ref page="course.md" %} 12 | 13 | {% page-ref page="person.md" %} 14 | 15 | {% page-ref page="public.md" %} 16 | 17 | -------------------------------------------------------------------------------- /docs/apis/card.md: -------------------------------------------------------------------------------- 1 | # 一卡通 API 2 | 3 | 此章节是关于一卡通 API 的描述。 4 | 5 | ## `Card` objects 6 | 7 | [`Card`](card.md#card-objects) 对象提供了一系列一卡通信息 API。 8 | 9 | **Constructor**: 10 | 11 | _class_ hdu\_api.**Card**(*session*) 12 | 13 | | 参数 | type | required | default | 备注 | 14 | | :---: | :---: | :---: | :---: | :---: | 15 | | session | objects `CardSession` | true | 无 | 一个已初始化的 `CardSession` 对象 16 | 17 | **Class methods**: 18 | 19 | - _classmethod_ Card.**account**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 20 | 21 | 一卡通账户信息。 22 | 23 | - _classmethod_ Card.**balance**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 24 | 25 | 一卡通余额。 26 | 27 | - _classmethod_ Card.**consume**(*year, month, raw=False, dictionary=DEFAULT_DICTIONARY*) 28 | 29 | 某年某月一卡通流水。 30 | 31 | - _classmethod_ Card.**consume_today**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 32 | 33 | 今日一卡通流水。 34 | 35 | - _classmethod_ Card.**statistics**(*year, month, raw=False, dictionary=DEFAULT_DICTIONARY*) 36 | 37 | 月交易统计。 38 | 39 | 参数说明: 40 | 41 | - raw - 是否输出为原始格式 42 | - dictionary - raw 为 true 时,使用该字典替换返回数据的 key 43 | - year - 年份,如 2019 44 | - month - 月份,如 1, 3, 4, 12 45 | 46 | **Class attributes**: 47 | 48 | - Card.session 49 | 50 | `CardSession` 对象。 51 | -------------------------------------------------------------------------------- /docs/apis/course.md: -------------------------------------------------------------------------------- 1 | # 课程 API 2 | 3 | 此章节是关于课程 API 的描述。 4 | 5 | ## `Course` objects 6 | 7 | [`Course`](course.md#course-objects) 对象提供了一系列课程信息 API。 8 | 9 | **Constructor**: 10 | 11 | _class_ hdu\_api.**Course**(*session*) 12 | 13 | | 参数 | type | required | default | 备注 | 14 | | :---: | :---: | :---: | :---: | :---: | 15 | | session | objects `CourseSession` | true | 无 | 一个已初始化的 `CourseSession` 对象 16 | 17 | **Class methods**: 18 | 19 | - _classmethod_ Course.**selected**(_year, term, raw=False, dictionary=DEFAULT\_DICTIONARY_) 20 | 21 | 已选课程。 22 | 23 | - _classmethod_ Course.**selected_current**(_raw=False, dictionary=DEFAULT\_DICTIONARY_) 24 | 25 | 本学期已选课程。 26 | 27 | - _classmethod_ Course.**schedule**(_year, term, raw=False, dictionary=DEFAULT\_DICTIONARY_) 28 | 29 | 课表。 30 | 31 | - _classmethod_ Course.**schedule_current**(_raw=False, dictionary=DEFAULT\_DICTIONARY_) 32 | 33 | 本学期课表。 34 | 35 | 参数说明: 36 | 37 | - raw - 是否输出为原始格式 38 | - dictionary - raw 为 true 时,使用该字典替换返回数据的 key 39 | - year - 学年,格式如 '2018-2019' 40 | - term - 学期,1 表示第一个学期,2 表示第二个学期 41 | 42 | **Class attributes**: 43 | 44 | - Course.**session** 45 | 46 | `CourseSession` 对象。 -------------------------------------------------------------------------------- /docs/apis/create.md: -------------------------------------------------------------------------------- 1 | # hdu\_api 2 | 3 | 此章节是关于如何创建一个 client 的描述。 4 | 5 | ## `HDU` Objects 6 | 7 | [`HDU`](create.md#hdu-objects) 对象是使用 `hdu-api` 的起点,它提供了创建 `HduClient` 对象的方法。 8 | 9 | **Constructor**: 10 | 11 | _class_ hdu\_api.**HDU**(_username, password, \*\*kwargs_) 12 | 13 | | 参数 | type | required | default | 备注 | 14 | | :---: | :---: | :---: | :---: | :---: | 15 | | username | str | true | 无 | 学号 | 16 | | password | str | true | 无 | 密码 | 17 | | kwargs | dict | false | 无 | | 18 | 19 | **Class methods**: 20 | 21 | - _classmethod_ HDU.**create**\(_\*args_) 22 | 23 | 返回一个 `HduClient` 对象。 24 | 25 | **Class attributes**: 26 | 27 | - HDU.**username** 28 | 29 | 学号,字符串类型。 30 | 31 | - HDU.**password** 32 | 33 | 密码,数字杭电的密码,字符串类型。 34 | 35 | ## `HduClient` Objects 36 | 37 | [`HduClient`](create.md#hduclient-objects) 对象提供了对 API 访问的通道。 38 | 39 | **Constructor**: 40 | 41 | _class_ hdu\_api.**HduClient**(_sess\_mgr, \*\*kwargs_) 42 | 43 | | 参数 | type | required | default | 备注 | 44 | | :---: | :---: | :---: | :---: | :---: | 45 | | sess\_mgr | objects `SessionManager` | true | 无 | | 46 | | kwargs | dict | false | 无 | | 47 | 48 | **Class methods**: 49 | 50 | 51 | **Class attributes**: 52 | 53 | - HduClient.**sess\_mgr** 54 | 55 | `SessionManager` 对象,提供了 session 管理。 56 | 57 | - HduClient.**username** 58 | 59 | 学号,字符串类型。 60 | 61 | - HduClient.**card** 62 | 63 | `Card` 对象,提供了对一卡通 API 的访问,详情请查看[这里](card.md)。 64 | 65 | - HduClient.**exam** 66 | 67 | `Exam` 对象,提供了对考试 API 的访问,详情请查看[这里](exam.md)。 68 | 69 | - HduClient.**person** 70 | 71 | `Person` 对象,提供了对个人信息 API 的访问,详情请查看[这里](person.md)。 72 | 73 | - HduClient.**course** 74 | 75 | `Course` 对象,提供了对课程 API 的访问,详情请查看[这里](course.md)。 76 | 77 | - HduClient.**public** 78 | 79 | `Public` 对象,提供了对公共信息 API 的访问,详情请查看[这里](public.md)。 80 | -------------------------------------------------------------------------------- /docs/apis/exam.md: -------------------------------------------------------------------------------- 1 | # 考试 API 2 | 3 | 此章节是关于考试 API 的描述。 4 | 5 | ## `Exam` objects 6 | 7 | [`Exam`](exam.md#exam-objects) 对象提供了一系列考试信息 API。 8 | 9 | **Constructor**: 10 | 11 | _class_ hdu\_api.**Exam**(*session*) 12 | 13 | | 参数 | type | required | default | 备注 | 14 | | :---: | :---: | :---: | :---: | :---: | 15 | | session | objects `ExamSession` | true | 无 | 一个已初始化的 `ExamSession` 对象 16 | 17 | **Class methods**: 18 | 19 | - _classmethod_ Exam.**grade**(*year, term, raw=False, dictionary=DEFAULT_DICTIONARY*) 20 | 21 | 学期成绩。 22 | 23 | - _classmethod_ Exam.**grade_current**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 24 | 25 | 本学期成绩。 26 | 27 | - _classmethod_ Exam.**level_grade**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 28 | 29 | 等级考试成绩,如 CET-4 成绩。 30 | 31 | - _classmethod_ Exam.**schedule**(*year, term, raw=False, dictionary=DEFAULT_DICTIONARY*) 32 | 33 | 考试安排。 34 | 35 | - _classmethod_ Exam.**schedule_current**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 36 | 37 | 本学期考试安排。 38 | 39 | - _classmethod_ Exam.**schedule_make_up**(*term, raw=False, dictionary=DEFAULT_DICTIONARY*) 40 | 41 | 补考安排。 42 | 43 | 参数说明: 44 | 45 | - raw - 是否输出为原始格式 46 | - dictionary - raw 为 true 时,使用该字典替换返回数据的 key 47 | - year - 学年,格式如 '2018-2019' 48 | - term - 学期,1 表示第一个学期,2 表示第二个学期 49 | 50 | **Class attributes**: 51 | 52 | - Exam.**session** 53 | 54 | `ExamSession` 对象。 55 | -------------------------------------------------------------------------------- /docs/apis/person.md: -------------------------------------------------------------------------------- 1 | # 个人信息 API 2 | 3 | 此章节是关于个人信息 API 的描述。 4 | 5 | ## `Person` objects 6 | 7 | [`Person`](person.md#person-objects) 对象提供了一系列个人信息 API。 8 | 9 | **Constructor**: 10 | 11 | _class_ hdu\_api.**Person**(*session*) 12 | 13 | | 参数 | type | required | default | 备注 | 14 | | :---: | :---: | :---: | :---: | :---: | 15 | | session | objects `PersonSession` | true | 无 | 一个已初始化的 `PersonSession` 对象 16 | 17 | **Class methods**: 18 | 19 | - _classmethod_ Person.**profile**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 20 | 21 | 基本个人信息。 22 | 23 | - _classmethod_ Person.**instructor**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 24 | 25 | 辅导员信息。 26 | 27 | - _classmethod_ Person.**status**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 28 | 29 | 学籍信息。 30 | 31 | - _classmethod_ Person.**accommodation**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 32 | 33 | 住宿信息。 34 | 35 | - _classmethod_ Person.**award**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 36 | 37 | 奖项信息。 38 | 39 | - _classmethod_ Person.**profile_all**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 40 | 41 | 全部个人信息,即以上信息的集合。 42 | 43 | 参数说明: 44 | 45 | - raw - 是否输出为原始格式 46 | - dictionary - raw 为 true 时,使用该字典替换返回数据的 key 47 | 48 | **Class attributes**: 49 | 50 | - Person.**session** 51 | 52 | `PersonSession` 对象。 -------------------------------------------------------------------------------- /docs/apis/public.md: -------------------------------------------------------------------------------- 1 | # 公共信息 API 2 | 3 | 此章节是关于公共信息 API 的描述。 4 | 5 | ## `Public` objects 6 | 7 | [`Public`](public.md#public-objects) 对象提供了一系列公共信息 API。 8 | 9 | **Constructor**: 10 | 11 | _class_ hdu\_api.**Public**(*session*) 12 | 13 | | 参数 | type | required | default | 备注 | 14 | | :---: | :---: | :---: | :---: | :---: | 15 | | session | objects `PublicSession` | true | 无 | 一个已初始化的 `PublicSession` 对象 16 | 17 | **Class methods**: 18 | 19 | - _classmethod_ Public.**classroom_free**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 20 | 21 | 空闲教室。 22 | 23 | - _classmethod_ Public.**classroom_in_use**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 24 | 25 | 在使用的教室。 26 | 27 | - _classmethod_ Public.**schooltime**(*location=0, raw=False, dictionary=DEFAULT_DICTIONARY*) 28 | 29 | 上课时间表。 30 | 31 | 参数说明: 32 | 33 | - raw - 是否输出为原始格式 34 | - dictionary - raw 为 true 时,使用该字典替换返回数据的 key 35 | - location - 校区,0 表示下沙, 1 表示青山湖 36 | 37 | **Class attributes**: 38 | 39 | - Public.**session** 40 | 41 | `PublicSession` 对象。 -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # changelog 2 | 3 | ## 0.0.1b1 - 2019-02-13 4 | 5 | ### Fixed 6 | 7 | * 修复对 Python2 的兼容性 8 | 9 | ## 0.0.1b0 - 2019-02-11 10 | 11 | ### Added 12 | 13 | * 提供对 HDU 所有教务管理信息 API 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # 安装 2 | 3 | 这一部分文档包含了 `hdu-api` 的安装方式。 4 | 5 | ## pip install hdu-api 6 | 7 | 使用包管理器 `pip` 安装 `hdu-api`,在你的终端运行这个简单的命令: 8 | 9 | ```text 10 | $ pip install hdu-api 11 | ``` 12 | 13 | ## 从源码安装 14 | 15 | `hdu-api` 的源代码公布在 Github 上,你从[这里](https://github.com/Cyrus97/hdu-api)可以获取到最新的代码。 16 | 17 | 你可以先克隆该仓库: 18 | 19 | ```text 20 | $ git clone https://github.com/Cyrus97/hdu-api 21 | ``` 22 | 23 | 之后再从源码安装: 24 | 25 | ```text 26 | $ cd hdu-api 27 | 28 | # 确保当前目录在 hdu-api 文件夹 29 | $ pip install . 30 | ``` 31 | 32 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # 快速上手 2 | 3 | 这一文档部分介绍了如何快速上手 `hdu-api`。现已假设你已安装 `hdu-api`,如果还没有安装,请去[安装](install.md)一节进行安装。 4 | 5 | 现在让我们从一些简单的示例开始吧! 6 | 7 | ## 创建 client 8 | 9 | 要想使用 `hdu-api` 访问到一卡通、课表等信息,必须先创建一个 `client`。 10 | 11 | 首先我们先导入使用的模块: 12 | 13 | ```text 14 | >>> from hdu_api import HDU 15 | ``` 16 | 17 | 之后传入正确的 `学号` 和 `密码` 创建一个 `HDU` 对象: 18 | 19 | ```text 20 | >>> hdu = HDU('学号', '密码') 21 | ``` 22 | 23 | 最后使用该对象创建一个 client: 24 | 25 | ```text 26 | >>> client = hdu.create() 27 | ``` 28 | 29 | 我们最后得到了一个名为 `client` 的 `HduClient` 对象,通过该对象可以获取到任意的信息。 30 | 31 | ## 访问信息 32 | 33 | 通过上面,我们已经创建了一个名为 `client` 的 `HduClient` 对象,现在我们看看如何访问我们想要的信息。 34 | 35 | ### 获取一卡通相关的信息 36 | 37 | 通过 `HduClient.card` 可以获取到使用一卡通的信息。 38 | 39 | 一卡通账户: 40 | 41 | ```text 42 | >>> from hdu_api import HDU 43 | >>> hdu = HDU('学号', '密码') 44 | >>> client = hdu.create() 45 | >>> client.card.account() 46 | [{'account_id': '30***86', 47 | 'card_id': '13***42', 48 | 'card_type': 'M1', 49 | 'department': '1***7', 50 | 'expiry_date': '2020年8月29日', 51 | 'gender': '*', 52 | 'id_card': '', 53 | 'id_type': '', 54 | 'identity': '***', 55 | 'staff_id': '1***7', 56 | 'staff_name': '***', 57 | 'status': '有效卡'}] 58 | ``` 59 | 60 | 一卡通余额: 61 | 62 | ```text 63 | >>> client.card.balance() 64 | [{'account_id': '3***6', 65 | 'balance': '76.07', 66 | 'card_id': '1***2', 67 | 'staff_id': '1***7', 68 | 'staff_name': '***'}] 69 | ``` 70 | 71 | 更多一卡通信息 API 使用可以查看[这里](apis/card.md)。 72 | 73 | ### 获取考试相关的信息 74 | 75 | 通过 `HduClient.exam` 可以获取到使用考试相关的信息。 76 | 77 | 考试成绩: 78 | 79 | ```text 80 | >>> client.exam.grade_current() 81 | [{'college': '外国语学院', 82 | 'comments': '', 83 | 'comments_of_makeup': '', 84 | 'course_attribution': '通识必修', 85 | 'course_code': 'A1103780', 86 | 'course_name': '实用翻译', 87 | 'course_type': '外语模块', 88 | 'credit': '2.0', 89 | 'experimental_score': '', 90 | 'final_score': '76.5', 91 | 'mid_score': '', 92 | 'retake_or_not': '', 93 | 'retest_mark': '', 94 | 'school_year': '2018-2019', 95 | 'score': '82', 96 | 'semester': '1', 97 | 'usual_score': '91'}, 98 | 99 | ... 100 | 101 | {'college': '计算机学院', 102 | 'comments': '', 103 | 'comments_of_makeup': '', 104 | 'course_attribution': '', 105 | 'course_code': 'B0507370', 106 | 'course_name': '数据挖掘', 107 | 'course_type': '专业限选', 108 | 'credit': '3.0', 109 | 'experimental_score': '', 110 | 'final_score': '85', 111 | 'mid_score': '', 112 | 'retake_or_not': '', 113 | 'retest_mark': '', 114 | 'school_year': '2018-2019', 115 | 'score': '84', 116 | 'semester': '1', 117 | 'usual_score': '83'}] 118 | ``` 119 | 120 | 考试安排: 121 | 122 | ```text 123 | >>> client.exam.schedule_current() 124 | [{'classroom': '第12教研楼201', 125 | 'course_name': '操作系统(甲)', 126 | 'exam_time': '2019年1月17日(09:00-11:00)', 127 | 'exam_type': '', 128 | 'seat': '10', 129 | 'select_code': '(2018-2019-1)-A0507050-06018-1', 130 | 'staff_name': 'xxx'}, 131 | 132 | ... 133 | 134 | {'classroom': '第6教研楼北308', 135 | 'course_name': '软件工程(甲)', 136 | 'exam_time': '2019年1月9日(13:45-15:45)', 137 | 'exam_type': '', 138 | 'seat': '24', 139 | 'select_code': '(2018-2019-1)-A0507190-06061-2', 140 | 'staff_name': 'xxx'}] 141 | ``` 142 | 143 | 更多考试相关信息 API 使用可以查看[这里](apis/exam.md)。 144 | 145 | {% hint style="info" %} 146 | 当然,`hdu-api` 提供的 API 访问服务不仅仅只有这些,想查看所有的 API 使用吗?请访问[这里](apis/)。 147 | {% endhint %} 148 | 149 | -------------------------------------------------------------------------------- /hdu_api/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # __ __ ____ __ __ ___ ____ ____ 5 | # / / / // __ \ / / / / / | / __ \ / _/ 6 | # / /_/ // / / // / / /______ / /| | / /_/ / / / 7 | # / __ // /_/ // /_/ //_____// ___ | / ____/_/ / 8 | # /_/ /_//_____/ \____/ /_/ |_|/_/ /___/ 9 | # 10 | # 11 | 12 | """ 13 | HDU-API 14 | ------- 15 | 16 | HDU-API is a simple sdk for hdu to access the information in the school's websites, written in Python, 17 | basing on the requests library and the bs4 library mainly. 18 | """ 19 | 20 | hard_dependencies = ("requests", "bs4", "lxml") 21 | missing_dependencies = [] 22 | 23 | for dependency in hard_dependencies: 24 | try: 25 | __import__(dependency) 26 | except ImportError as e: 27 | missing_dependencies.append(dependency) 28 | 29 | if missing_dependencies: 30 | raise ImportError( 31 | "Missing required dependencies {0}".format(missing_dependencies)) 32 | del hard_dependencies, missing_dependencies 33 | 34 | from .__version__ import __title__, __description__, __url__, __version__ 35 | from .__version__ import __build__, __author__, __author_email__, __license__ 36 | 37 | from .api import HDU, HduClient 38 | from .sessions import SessionManager, TeachingSession, CardSession, StudentSession, IHDUSession, IHDUPhoneSession 39 | from .models import Card, Exam, Course, Person, Public, get_current_term 40 | -------------------------------------------------------------------------------- /hdu_api/__version__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __title__ = 'hdu-api' 4 | __description__ = 'A simple SDK for HDU.' 5 | __url__ = 'https://github.com/Cyrus97/hdu-api' 6 | __version__ = '0.1.0' 7 | __build__ = 0x000100 8 | __author__ = 'Cyrus Liu' 9 | __author_email__ = 'liuxingran97@gmail.com' 10 | __license__ = 'MIT' 11 | -------------------------------------------------------------------------------- /hdu_api/_internal_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | hdu_api._internal_utils 5 | ----------------------- 6 | 7 | 8 | """ 9 | import sys 10 | 11 | from hdu_api import _pyDes 12 | 13 | _ver = sys.version_info 14 | 15 | #: Python 2.x? 16 | is_py2 = (_ver[0] == 2) 17 | 18 | #: Python 3.x? 19 | is_py3 = (_ver[0] == 3) 20 | 21 | 22 | def encrypt(data, first_key, second_key, third_key): 23 | bts_data = extend_to_16bits(data) 24 | bts_first_key = extend_to_16bits(first_key) 25 | bts_second_key = extend_to_16bits(second_key) 26 | bts_third_key = extend_to_16bits(third_key) 27 | i = 0 28 | bts_result = [] 29 | while i < len(bts_data): 30 | # 将data分成每64位一段,分段加密 31 | bts_temp = bts_data[i:i + 8] 32 | j, k, z = 0, 0, 0 33 | while j < len(bts_first_key): 34 | # 分别取出 first_key 的64位作为密钥 35 | des_k = _pyDes.des(bts_first_key[j: j + 8], _pyDes.ECB) 36 | bts_temp = list(des_k.encrypt(bts_temp)) 37 | j += 8 38 | while k < len(bts_second_key): 39 | des_k = _pyDes.des(bts_second_key[k:k + 8], _pyDes.ECB) 40 | bts_temp = list(des_k.encrypt(bts_temp)) 41 | k += 8 42 | while z < len(bts_third_key): 43 | des_k = _pyDes.des(bts_third_key[z:z + 8], _pyDes.ECB) 44 | bts_temp = list(des_k.encrypt(bts_temp)) 45 | z += 8 46 | 47 | bts_result.extend(bts_temp) 48 | i += 8 49 | str_result = '' 50 | for each in bts_result: 51 | if is_py2: 52 | each = ord(each) 53 | # 分别加密data的各段,串联成字符串 54 | str_result += '%02X' % each 55 | return str_result 56 | 57 | 58 | def extend_to_16bits(data): 59 | """ 60 | 将字符串的每个字符前插入 0,变成16位,并在后面补0,使其长度是64位整数倍 61 | :param data: 62 | :return: 63 | """ 64 | bts = data.encode() 65 | c = 0 66 | if is_py2: 67 | c = chr(c) 68 | filled_bts = [] 69 | for each in bts: 70 | # 每个字符前插入 0 71 | filled_bts.extend([c, each]) 72 | # 长度扩展到8的倍数,若不是8的倍数,后面添加0,便于DES加密时分组 73 | while len(filled_bts) % 8 != 0: 74 | filled_bts.append(c) 75 | return filled_bts 76 | -------------------------------------------------------------------------------- /hdu_api/_pyDes.py: -------------------------------------------------------------------------------- 1 | ############################################################################# 2 | # Documentation # 3 | ############################################################################# 4 | 5 | # Author: Todd Whiteman 6 | # Date: 16th March, 2009 7 | # Verion: 2.0.0 8 | # License: MIT 9 | # Homepage: http://twhiteman.netfirms.com/des.html 10 | # 11 | # This is a pure python implementation of the DES encryption algorithm. 12 | # It's pure python to avoid portability issues, since most DES 13 | # implementations are programmed in C (for performance reasons). 14 | # 15 | # Triple DES class is also implemented, utilising the DES base. Triple DES 16 | # is either DES-EDE3 with a 24 byte key, or DES-EDE2 with a 16 byte key. 17 | # 18 | # See the README.txt that should come with this python module for the 19 | # implementation methods used. 20 | # 21 | # Thanks to: 22 | # * David Broadwell for ideas, comments and suggestions. 23 | # * Mario Wolff for pointing out and debugging some triple des CBC errors. 24 | # * Santiago Palladino for providing the PKCS5 padding technique. 25 | # * Shaya for correcting the PAD_PKCS5 triple des CBC errors. 26 | # 27 | """A pure python implementation of the DES and TRIPLE DES encryption algorithms. 28 | 29 | Class initialization 30 | -------------------- 31 | pyDes.des(key, [mode], [IV], [pad], [padmode]) 32 | pyDes.triple_des(key, [mode], [IV], [pad], [padmode]) 33 | 34 | key -> Bytes containing the encryption key. 8 bytes for DES, 16 or 24 bytes 35 | for Triple DES 36 | mode -> Optional argument for encryption type, can be either 37 | pyDes.ECB (Electronic Code Book) or pyDes.CBC (Cypher Block Chaining) 38 | IV -> Optional Initial Value bytes, must be supplied if using CBC mode. 39 | Length must be 8 bytes. 40 | pad -> Optional argument, set the pad character (PAD_NORMAL) to use during 41 | all encrypt/decrpt operations done with this instance. 42 | padmode -> Optional argument, set the padding mode (PAD_NORMAL or PAD_PKCS5) 43 | to use during all encrypt/decrpt operations done with this instance. 44 | 45 | I recommend to use PAD_PKCS5 padding, as then you never need to worry about any 46 | padding issues, as the padding can be removed unambiguously upon decrypting 47 | data that was encrypted using PAD_PKCS5 padmode. 48 | 49 | Common methods 50 | -------------- 51 | encrypt(data, [pad], [padmode]) 52 | decrypt(data, [pad], [padmode]) 53 | 54 | data -> Bytes to be encrypted/decrypted 55 | pad -> Optional argument. Only when using padmode of PAD_NORMAL. For 56 | encryption, adds this characters to the end of the data block when 57 | data is not a multiple of 8 bytes. For decryption, will remove the 58 | trailing characters that match this pad character from the last 8 59 | bytes of the unencrypted data block. 60 | padmode -> Optional argument, set the padding mode, must be one of PAD_NORMAL 61 | or PAD_PKCS5). Defaults to PAD_NORMAL. 62 | 63 | 64 | Example 65 | ------- 66 | from pyDes import * 67 | 68 | data = "Please encrypt my data" 69 | k = des("DESCRYPT", CBC, "\0\0\0\0\0\0\0\0", pad=None, padmode=PAD_PKCS5) 70 | # For Python3, you'll need to use bytes, i.e.: 71 | # data = b"Please encrypt my data" 72 | # k = des(b"DESCRYPT", CBC, b"\0\0\0\0\0\0\0\0", pad=None, padmode=PAD_PKCS5) 73 | d = k.encrypt(data) 74 | print "Encrypted: %r" % d 75 | print "Decrypted: %r" % k.decrypt(d) 76 | assert k.decrypt(d, padmode=PAD_PKCS5) == data 77 | 78 | 79 | See the module source (_pyDes.py) for more examples of use. 80 | You can also run the _pyDes.py file without and arguments to see a simple test. 81 | 82 | Note: This code was not written for high-end systems needing a fast 83 | implementation, but rather a handy portable solution with small usage. 84 | 85 | """ 86 | 87 | import sys 88 | 89 | # _pythonMajorVersion is used to handle Python2 and Python3 differences. 90 | _pythonMajorVersion = sys.version_info[0] 91 | 92 | # Modes of crypting / cyphering 93 | ECB = 0 94 | CBC = 1 95 | 96 | # Modes of padding 97 | PAD_NORMAL = 1 98 | PAD_PKCS5 = 2 99 | 100 | 101 | # PAD_PKCS5: is a method that will unambiguously remove all padding 102 | # characters after decryption, when originally encrypted with 103 | # this padding mode. 104 | # For a good description of the PKCS5 padding technique, see: 105 | # http://www.faqs.org/rfcs/rfc1423.html 106 | 107 | # The base class shared by des and triple des. 108 | class _baseDes(object): 109 | def __init__(self, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): 110 | if IV: 111 | IV = self._guardAgainstUnicode(IV) 112 | if pad: 113 | pad = self._guardAgainstUnicode(pad) 114 | self.block_size = 8 115 | # Sanity checking of arguments. 116 | if pad and padmode == PAD_PKCS5: 117 | raise ValueError("Cannot use a pad character with PAD_PKCS5") 118 | if IV and len(IV) != self.block_size: 119 | raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes") 120 | 121 | # Set the passed in variables 122 | self._mode = mode 123 | self._iv = IV 124 | self._padding = pad 125 | self._padmode = padmode 126 | 127 | def getKey(self): 128 | """getKey() -> bytes""" 129 | return self.__key 130 | 131 | def setKey(self, key): 132 | """Will set the crypting key for this object.""" 133 | key = self._guardAgainstUnicode(key) 134 | self.__key = key 135 | 136 | def getMode(self): 137 | """getMode() -> pyDes.ECB or pyDes.CBC""" 138 | return self._mode 139 | 140 | def setMode(self, mode): 141 | """Sets the type of crypting mode, pyDes.ECB or pyDes.CBC""" 142 | self._mode = mode 143 | 144 | def getPadding(self): 145 | """getPadding() -> bytes of length 1. Padding character.""" 146 | return self._padding 147 | 148 | def setPadding(self, pad): 149 | """setPadding() -> bytes of length 1. Padding character.""" 150 | if pad is not None: 151 | pad = self._guardAgainstUnicode(pad) 152 | self._padding = pad 153 | 154 | def getPadMode(self): 155 | """getPadMode() -> pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" 156 | return self._padmode 157 | 158 | def setPadMode(self, mode): 159 | """Sets the type of padding mode, pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" 160 | self._padmode = mode 161 | 162 | def getIV(self): 163 | """getIV() -> bytes""" 164 | return self._iv 165 | 166 | def setIV(self, IV): 167 | """Will set the Initial Value, used in conjunction with CBC mode""" 168 | if not IV or len(IV) != self.block_size: 169 | raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes") 170 | IV = self._guardAgainstUnicode(IV) 171 | self._iv = IV 172 | 173 | def _padData(self, data, pad, padmode): 174 | # Pad data depending on the mode 175 | if padmode is None: 176 | # Get the default padding mode. 177 | padmode = self.getPadMode() 178 | if pad and padmode == PAD_PKCS5: 179 | raise ValueError("Cannot use a pad character with PAD_PKCS5") 180 | 181 | if padmode == PAD_NORMAL: 182 | if len(data) % self.block_size == 0: 183 | # No padding required. 184 | return data 185 | 186 | if not pad: 187 | # Get the default padding. 188 | pad = self.getPadding() 189 | if not pad: 190 | raise ValueError("Data must be a multiple of " + str( 191 | self.block_size) + " bytes in length. Use padmode=PAD_PKCS5 or set the pad character.") 192 | data += (self.block_size - (len(data) % self.block_size)) * pad 193 | 194 | elif padmode == PAD_PKCS5: 195 | pad_len = 8 - (len(data) % self.block_size) 196 | if _pythonMajorVersion < 3: 197 | data += pad_len * chr(pad_len) 198 | else: 199 | data += bytes([pad_len] * pad_len) 200 | 201 | return data 202 | 203 | def _unpadData(self, data, pad, padmode): 204 | # Unpad data depending on the mode. 205 | if not data: 206 | return data 207 | if pad and padmode == PAD_PKCS5: 208 | raise ValueError("Cannot use a pad character with PAD_PKCS5") 209 | if padmode is None: 210 | # Get the default padding mode. 211 | padmode = self.getPadMode() 212 | 213 | if padmode == PAD_NORMAL: 214 | if not pad: 215 | # Get the default padding. 216 | pad = self.getPadding() 217 | if pad: 218 | data = data[:-self.block_size] + \ 219 | data[-self.block_size:].rstrip(pad) 220 | 221 | elif padmode == PAD_PKCS5: 222 | if _pythonMajorVersion < 3: 223 | pad_len = ord(data[-1]) 224 | else: 225 | pad_len = data[-1] 226 | data = data[:-pad_len] 227 | 228 | return data 229 | 230 | def _guardAgainstUnicode(self, data): 231 | # Only accept byte strings or ascii unicode values, otherwise 232 | # there is no way to correctly decode the data into bytes. 233 | if _pythonMajorVersion < 3: 234 | if isinstance(data, unicode): 235 | raise ValueError("pyDes can only work with bytes, not Unicode strings.") 236 | else: 237 | if isinstance(data, str): 238 | # Only accept ascii unicode values. 239 | try: 240 | return data.encode('ascii') 241 | except UnicodeEncodeError: 242 | pass 243 | raise ValueError("pyDes can only work with encoded strings, not Unicode.") 244 | return data 245 | 246 | 247 | ############################################################################# 248 | # DES # 249 | ############################################################################# 250 | class des(_baseDes): 251 | """DES encryption/decrytpion class 252 | 253 | Supports ECB (Electronic Code Book) and CBC (Cypher Block Chaining) modes. 254 | 255 | pyDes.des(key,[mode], [IV]) 256 | 257 | key -> Bytes containing the encryption key, must be exactly 8 bytes 258 | mode -> Optional argument for encryption type, can be either pyDes.ECB 259 | (Electronic Code Book), pyDes.CBC (Cypher Block Chaining) 260 | IV -> Optional Initial Value bytes, must be supplied if using CBC mode. 261 | Must be 8 bytes in length. 262 | pad -> Optional argument, set the pad character (PAD_NORMAL) to use 263 | during all encrypt/decrpt operations done with this instance. 264 | padmode -> Optional argument, set the padding mode (PAD_NORMAL or 265 | PAD_PKCS5) to use during all encrypt/decrpt operations done 266 | with this instance. 267 | """ 268 | 269 | # Permutation and translation tables for DES 270 | __pc1 = [ 271 | 56, 48, 40, 32, 24, 16, 8, 0, 272 | 57, 49, 41, 33, 25, 17, 9, 1, 273 | 58, 50, 42, 34, 26, 18, 10, 2, 274 | 59, 51, 43, 35, 27, 19, 11, 3, 275 | 60, 52, 44, 36, 28, 20, 12, 4, 276 | 61, 53, 45, 37, 29, 21, 13, 5, 277 | 62, 54, 46, 38, 30, 22, 14, 6 278 | ] 279 | 280 | # number left rotations of pc1 281 | __left_rotations = [ 282 | 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1 283 | ] 284 | 285 | # permuted choice key (table 2) 286 | __pc2 = [ 287 | 13, 16, 10, 23, 0, 4, 288 | 2, 27, 14, 5, 20, 9, 289 | 22, 18, 11, 3, 25, 7, 290 | 15, 6, 26, 19, 12, 1, 291 | 40, 51, 30, 36, 46, 54, 292 | 29, 39, 50, 44, 32, 47, 293 | 43, 48, 38, 55, 33, 52, 294 | 45, 41, 49, 35, 28, 31 295 | ] 296 | 297 | # initial permutation IP 298 | __ip = [57, 49, 41, 33, 25, 17, 9, 1, 299 | 59, 51, 43, 35, 27, 19, 11, 3, 300 | 61, 53, 45, 37, 29, 21, 13, 5, 301 | 63, 55, 47, 39, 31, 23, 15, 7, 302 | 56, 48, 40, 32, 24, 16, 8, 0, 303 | 58, 50, 42, 34, 26, 18, 10, 2, 304 | 60, 52, 44, 36, 28, 20, 12, 4, 305 | 62, 54, 46, 38, 30, 22, 14, 6 306 | ] 307 | 308 | # Expansion table for turning 32 bit blocks into 48 bits 309 | __expansion_table = [ 310 | 31, 0, 1, 2, 3, 4, 311 | 3, 4, 5, 6, 7, 8, 312 | 7, 8, 9, 10, 11, 12, 313 | 11, 12, 13, 14, 15, 16, 314 | 15, 16, 17, 18, 19, 20, 315 | 19, 20, 21, 22, 23, 24, 316 | 23, 24, 25, 26, 27, 28, 317 | 27, 28, 29, 30, 31, 0 318 | ] 319 | 320 | # The (in)famous S-boxes 321 | __sbox = [ 322 | # S1 323 | [14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, 324 | 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, 325 | 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, 326 | 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13], 327 | 328 | # S2 329 | [15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, 330 | 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5, 331 | 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, 332 | 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9], 333 | 334 | # S3 335 | [10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, 336 | 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, 337 | 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, 338 | 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12], 339 | 340 | # S4 341 | [7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, 342 | 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, 343 | 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, 344 | 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14], 345 | 346 | # S5 347 | [2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, 348 | 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, 349 | 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, 350 | 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3], 351 | 352 | # S6 353 | [12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, 354 | 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, 355 | 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, 356 | 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13], 357 | 358 | # S7 359 | [4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, 360 | 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, 361 | 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, 362 | 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12], 363 | 364 | # S8 365 | [13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, 366 | 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, 367 | 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, 368 | 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11], 369 | ] 370 | 371 | # 32-bit permutation function P used on the output of the S-boxes 372 | __p = [ 373 | 15, 6, 19, 20, 28, 11, 374 | 27, 16, 0, 14, 22, 25, 375 | 4, 17, 30, 9, 1, 7, 376 | 23, 13, 31, 26, 2, 8, 377 | 18, 12, 29, 5, 21, 10, 378 | 3, 24 379 | ] 380 | 381 | # final permutation IP^-1 382 | __fp = [ 383 | 39, 7, 47, 15, 55, 23, 63, 31, 384 | 38, 6, 46, 14, 54, 22, 62, 30, 385 | 37, 5, 45, 13, 53, 21, 61, 29, 386 | 36, 4, 44, 12, 52, 20, 60, 28, 387 | 35, 3, 43, 11, 51, 19, 59, 27, 388 | 34, 2, 42, 10, 50, 18, 58, 26, 389 | 33, 1, 41, 9, 49, 17, 57, 25, 390 | 32, 0, 40, 8, 48, 16, 56, 24 391 | ] 392 | 393 | # Type of crypting being done 394 | ENCRYPT = 0x00 395 | DECRYPT = 0x01 396 | 397 | # Initialisation 398 | def __init__(self, key, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): 399 | # Sanity checking of arguments. 400 | if len(key) != 8: 401 | raise ValueError("Invalid DES key size. Key must be exactly 8 bytes long.") 402 | _baseDes.__init__(self, mode, IV, pad, padmode) 403 | self.key_size = 8 404 | 405 | self.L = [] 406 | self.R = [] 407 | self.Kn = [[0] * 48] * 16 # 16 48-bit keys (K1 - K16) 408 | self.final = [] 409 | 410 | self.setKey(key) 411 | 412 | def setKey(self, key): 413 | """Will set the crypting key for this object. Must be 8 bytes.""" 414 | _baseDes.setKey(self, key) 415 | self.__create_sub_keys() 416 | 417 | def __String_to_BitList(self, data): 418 | """Turn the string data, into a list of bits (1, 0)'s""" 419 | if _pythonMajorVersion < 3: 420 | # Turn the strings into integers. Python 3 uses a bytes 421 | # class, which already has this behaviour. 422 | data = [ord(c) for c in data] 423 | l = len(data) * 8 424 | result = [0] * l 425 | pos = 0 426 | for ch in data: 427 | i = 7 428 | while i >= 0: 429 | if ch & (1 << i) != 0: 430 | result[pos] = 1 431 | else: 432 | result[pos] = 0 433 | pos += 1 434 | i -= 1 435 | 436 | return result 437 | 438 | def __BitList_to_String(self, data): 439 | """Turn the list of bits -> data, into a string""" 440 | result = [] 441 | pos = 0 442 | c = 0 443 | while pos < len(data): 444 | c += data[pos] << (7 - (pos % 8)) 445 | if (pos % 8) == 7: 446 | result.append(c) 447 | c = 0 448 | pos += 1 449 | 450 | if _pythonMajorVersion < 3: 451 | return ''.join([chr(c) for c in result]) 452 | else: 453 | return bytes(result) 454 | 455 | def __permutate(self, table, block): 456 | """Permutate this block with the specified table""" 457 | return list(map(lambda x: block[x], table)) 458 | 459 | # Transform the secret key, so that it is ready for data processing 460 | # Create the 16 subkeys, K[1] - K[16] 461 | def __create_sub_keys(self): 462 | """Create the 16 subkeys K[1] to K[16] from the given key""" 463 | key = self.__permutate(des.__pc1, self.__String_to_BitList(self.getKey())) 464 | i = 0 465 | # Split into Left and Right sections 466 | self.L = key[:28] 467 | self.R = key[28:] 468 | while i < 16: 469 | j = 0 470 | # Perform circular left shifts 471 | while j < des.__left_rotations[i]: 472 | self.L.append(self.L[0]) 473 | del self.L[0] 474 | 475 | self.R.append(self.R[0]) 476 | del self.R[0] 477 | 478 | j += 1 479 | 480 | # Create one of the 16 subkeys through pc2 permutation 481 | self.Kn[i] = self.__permutate(des.__pc2, self.L + self.R) 482 | 483 | i += 1 484 | 485 | # Main part of the encryption algorithm, the number cruncher :) 486 | def __des_crypt(self, block, crypt_type): 487 | """Crypt the block of data through DES bit-manipulation""" 488 | block = self.__permutate(des.__ip, block) 489 | self.L = block[:32] 490 | self.R = block[32:] 491 | 492 | # Encryption starts from Kn[1] through to Kn[16] 493 | if crypt_type == des.ENCRYPT: 494 | iteration = 0 495 | iteration_adjustment = 1 496 | # Decryption starts from Kn[16] down to Kn[1] 497 | else: 498 | iteration = 15 499 | iteration_adjustment = -1 500 | 501 | i = 0 502 | while i < 16: 503 | # Make a copy of R[i-1], this will later become L[i] 504 | tempR = self.R[:] 505 | 506 | # Permutate R[i - 1] to start creating R[i] 507 | self.R = self.__permutate(des.__expansion_table, self.R) 508 | 509 | # Exclusive or R[i - 1] with K[i], create B[1] to B[8] whilst here 510 | self.R = list(map(lambda x, y: x ^ y, self.R, self.Kn[iteration])) 511 | B = [self.R[:6], self.R[6:12], self.R[12:18], self.R[18:24], self.R[24:30], self.R[30:36], self.R[36:42], 512 | self.R[42:]] 513 | # Optimization: Replaced below commented code with above 514 | # j = 0 515 | # B = [] 516 | # while j < len(self.R): 517 | # self.R[j] = self.R[j] ^ self.Kn[iteration][j] 518 | # j += 1 519 | # if j % 6 == 0: 520 | # B.append(self.R[j-6:j]) 521 | 522 | # Permutate B[1] to B[8] using the S-Boxes 523 | j = 0 524 | Bn = [0] * 32 525 | pos = 0 526 | while j < 8: 527 | # Work out the offsets 528 | m = (B[j][0] << 1) + B[j][5] 529 | n = (B[j][1] << 3) + (B[j][2] << 2) + (B[j][3] << 1) + B[j][4] 530 | 531 | # Find the permutation value 532 | v = des.__sbox[j][(m << 4) + n] 533 | 534 | # Turn value into bits, add it to result: Bn 535 | Bn[pos] = (v & 8) >> 3 536 | Bn[pos + 1] = (v & 4) >> 2 537 | Bn[pos + 2] = (v & 2) >> 1 538 | Bn[pos + 3] = v & 1 539 | 540 | pos += 4 541 | j += 1 542 | 543 | # Permutate the concatination of B[1] to B[8] (Bn) 544 | self.R = self.__permutate(des.__p, Bn) 545 | 546 | # Xor with L[i - 1] 547 | self.R = list(map(lambda x, y: x ^ y, self.R, self.L)) 548 | # Optimization: This now replaces the below commented code 549 | # j = 0 550 | # while j < len(self.R): 551 | # self.R[j] = self.R[j] ^ self.L[j] 552 | # j += 1 553 | 554 | # L[i] becomes R[i - 1] 555 | self.L = tempR 556 | 557 | i += 1 558 | iteration += iteration_adjustment 559 | 560 | # Final permutation of R[16]L[16] 561 | self.final = self.__permutate(des.__fp, self.R + self.L) 562 | return self.final 563 | 564 | # Data to be encrypted/decrypted 565 | def crypt(self, data, crypt_type): 566 | """Crypt the data in blocks, running it through des_crypt()""" 567 | 568 | # Error check the data 569 | if not data: 570 | return '' 571 | if len(data) % self.block_size != 0: 572 | if crypt_type == des.DECRYPT: # Decryption must work on 8 byte blocks 573 | raise ValueError( 574 | "Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n.") 575 | if not self.getPadding(): 576 | raise ValueError("Invalid data length, data must be a multiple of " + str( 577 | self.block_size) + " bytes\n. Try setting the optional padding character") 578 | else: 579 | data += (self.block_size - (len(data) % self.block_size)) * self.getPadding() 580 | # print "Len of data: %f" % (len(data) / self.block_size) 581 | 582 | if self.getMode() == CBC: 583 | if self.getIV(): 584 | iv = self.__String_to_BitList(self.getIV()) 585 | else: 586 | raise ValueError("For CBC mode, you must supply the Initial Value (IV) for ciphering") 587 | 588 | # Split the data into blocks, crypting each one seperately 589 | i = 0 590 | dict = {} 591 | result = [] 592 | # cached = 0 593 | # lines = 0 594 | while i < len(data): 595 | # Test code for caching encryption results 596 | # lines += 1 597 | # if dict.has_key(data[i:i+8]): 598 | # print "Cached result for: %s" % data[i:i+8] 599 | # cached += 1 600 | # result.append(dict[data[i:i+8]]) 601 | # i += 8 602 | # continue 603 | 604 | block = self.__String_to_BitList(data[i:i + 8]) 605 | 606 | # Xor with IV if using CBC mode 607 | if self.getMode() == CBC: 608 | if crypt_type == des.ENCRYPT: 609 | block = list(map(lambda x, y: x ^ y, block, iv)) 610 | # j = 0 611 | # while j < len(block): 612 | # block[j] = block[j] ^ iv[j] 613 | # j += 1 614 | 615 | processed_block = self.__des_crypt(block, crypt_type) 616 | 617 | if crypt_type == des.DECRYPT: 618 | processed_block = list(map(lambda x, y: x ^ y, processed_block, iv)) 619 | # j = 0 620 | # while j < len(processed_block): 621 | # processed_block[j] = processed_block[j] ^ iv[j] 622 | # j += 1 623 | iv = block 624 | else: 625 | iv = processed_block 626 | else: 627 | processed_block = self.__des_crypt(block, crypt_type) 628 | 629 | # Add the resulting crypted block to our list 630 | # d = self.__BitList_to_String(processed_block) 631 | # result.append(d) 632 | result.append(self.__BitList_to_String(processed_block)) 633 | # dict[data[i:i+8]] = d 634 | i += 8 635 | 636 | # print "Lines: %d, cached: %d" % (lines, cached) 637 | 638 | # Return the full crypted string 639 | if _pythonMajorVersion < 3: 640 | return ''.join(result) 641 | else: 642 | return bytes.fromhex('').join(result) 643 | 644 | def encrypt(self, data, pad=None, padmode=None): 645 | """encrypt(data, [pad], [padmode]) -> bytes 646 | 647 | data : Bytes to be encrypted 648 | pad : Optional argument for encryption padding. Must only be one byte 649 | padmode : Optional argument for overriding the padding mode. 650 | 651 | The data must be a multiple of 8 bytes and will be encrypted 652 | with the already specified key. Data does not have to be a 653 | multiple of 8 bytes if the padding character is supplied, or 654 | the padmode is set to PAD_PKCS5, as bytes will then added to 655 | ensure the be padded data is a multiple of 8 bytes. 656 | """ 657 | data = self._guardAgainstUnicode(data) 658 | if pad is not None: 659 | pad = self._guardAgainstUnicode(pad) 660 | data = self._padData(data, pad, padmode) 661 | return self.crypt(data, des.ENCRYPT) 662 | 663 | def decrypt(self, data, pad=None, padmode=None): 664 | """decrypt(data, [pad], [padmode]) -> bytes 665 | 666 | data : Bytes to be encrypted 667 | pad : Optional argument for decryption padding. Must only be one byte 668 | padmode : Optional argument for overriding the padding mode. 669 | 670 | The data must be a multiple of 8 bytes and will be decrypted 671 | with the already specified key. In PAD_NORMAL mode, if the 672 | optional padding character is supplied, then the un-encrypted 673 | data will have the padding characters removed from the end of 674 | the bytes. This pad removal only occurs on the last 8 bytes of 675 | the data (last data block). In PAD_PKCS5 mode, the special 676 | padding end markers will be removed from the data after decrypting. 677 | """ 678 | data = self._guardAgainstUnicode(data) 679 | if pad is not None: 680 | pad = self._guardAgainstUnicode(pad) 681 | data = self.crypt(data, des.DECRYPT) 682 | return self._unpadData(data, pad, padmode) 683 | 684 | 685 | ############################################################################# 686 | # Triple DES # 687 | ############################################################################# 688 | class triple_des(_baseDes): 689 | """Triple DES encryption/decrytpion class 690 | 691 | This algorithm uses the DES-EDE3 (when a 24 byte key is supplied) or 692 | the DES-EDE2 (when a 16 byte key is supplied) encryption methods. 693 | Supports ECB (Electronic Code Book) and CBC (Cypher Block Chaining) modes. 694 | 695 | pyDes.des(key, [mode], [IV]) 696 | 697 | key -> Bytes containing the encryption key, must be either 16 or 698 | 24 bytes long 699 | mode -> Optional argument for encryption type, can be either pyDes.ECB 700 | (Electronic Code Book), pyDes.CBC (Cypher Block Chaining) 701 | IV -> Optional Initial Value bytes, must be supplied if using CBC mode. 702 | Must be 8 bytes in length. 703 | pad -> Optional argument, set the pad character (PAD_NORMAL) to use 704 | during all encrypt/decrpt operations done with this instance. 705 | padmode -> Optional argument, set the padding mode (PAD_NORMAL or 706 | PAD_PKCS5) to use during all encrypt/decrpt operations done 707 | with this instance. 708 | """ 709 | 710 | def __init__(self, key, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): 711 | _baseDes.__init__(self, mode, IV, pad, padmode) 712 | self.setKey(key) 713 | 714 | def setKey(self, key): 715 | """Will set the crypting key for this object. Either 16 or 24 bytes long.""" 716 | self.key_size = 24 # Use DES-EDE3 mode 717 | if len(key) != self.key_size: 718 | if len(key) == 16: # Use DES-EDE2 mode 719 | self.key_size = 16 720 | else: 721 | raise ValueError("Invalid triple DES key size. Key must be either 16 or 24 bytes long") 722 | if self.getMode() == CBC: 723 | if not self.getIV(): 724 | # Use the first 8 bytes of the key 725 | self._iv = key[:self.block_size] 726 | if len(self.getIV()) != self.block_size: 727 | raise ValueError("Invalid IV, must be 8 bytes in length") 728 | self.__key1 = des(key[:8], self._mode, self._iv, 729 | self._padding, self._padmode) 730 | self.__key2 = des(key[8:16], self._mode, self._iv, 731 | self._padding, self._padmode) 732 | if self.key_size == 16: 733 | self.__key3 = self.__key1 734 | else: 735 | self.__key3 = des(key[16:], self._mode, self._iv, 736 | self._padding, self._padmode) 737 | _baseDes.setKey(self, key) 738 | 739 | # Override setter methods to work on all 3 keys. 740 | 741 | def setMode(self, mode): 742 | """Sets the type of crypting mode, pyDes.ECB or pyDes.CBC""" 743 | _baseDes.setMode(self, mode) 744 | for key in (self.__key1, self.__key2, self.__key3): 745 | key.setMode(mode) 746 | 747 | def setPadding(self, pad): 748 | """setPadding() -> bytes of length 1. Padding character.""" 749 | _baseDes.setPadding(self, pad) 750 | for key in (self.__key1, self.__key2, self.__key3): 751 | key.setPadding(pad) 752 | 753 | def setPadMode(self, mode): 754 | """Sets the type of padding mode, pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" 755 | _baseDes.setPadMode(self, mode) 756 | for key in (self.__key1, self.__key2, self.__key3): 757 | key.setPadMode(mode) 758 | 759 | def setIV(self, IV): 760 | """Will set the Initial Value, used in conjunction with CBC mode""" 761 | _baseDes.setIV(self, IV) 762 | for key in (self.__key1, self.__key2, self.__key3): 763 | key.setIV(IV) 764 | 765 | def encrypt(self, data, pad=None, padmode=None): 766 | """encrypt(data, [pad], [padmode]) -> bytes 767 | 768 | data : bytes to be encrypted 769 | pad : Optional argument for encryption padding. Must only be one byte 770 | padmode : Optional argument for overriding the padding mode. 771 | 772 | The data must be a multiple of 8 bytes and will be encrypted 773 | with the already specified key. Data does not have to be a 774 | multiple of 8 bytes if the padding character is supplied, or 775 | the padmode is set to PAD_PKCS5, as bytes will then added to 776 | ensure the be padded data is a multiple of 8 bytes. 777 | """ 778 | ENCRYPT = des.ENCRYPT 779 | DECRYPT = des.DECRYPT 780 | data = self._guardAgainstUnicode(data) 781 | if pad is not None: 782 | pad = self._guardAgainstUnicode(pad) 783 | # Pad the data accordingly. 784 | data = self._padData(data, pad, padmode) 785 | if self.getMode() == CBC: 786 | self.__key1.setIV(self.getIV()) 787 | self.__key2.setIV(self.getIV()) 788 | self.__key3.setIV(self.getIV()) 789 | i = 0 790 | result = [] 791 | while i < len(data): 792 | block = self.__key1.crypt(data[i:i + 8], ENCRYPT) 793 | block = self.__key2.crypt(block, DECRYPT) 794 | block = self.__key3.crypt(block, ENCRYPT) 795 | self.__key1.setIV(block) 796 | self.__key2.setIV(block) 797 | self.__key3.setIV(block) 798 | result.append(block) 799 | i += 8 800 | if _pythonMajorVersion < 3: 801 | return ''.join(result) 802 | else: 803 | return bytes.fromhex('').join(result) 804 | else: 805 | data = self.__key1.crypt(data, ENCRYPT) 806 | data = self.__key2.crypt(data, DECRYPT) 807 | return self.__key3.crypt(data, ENCRYPT) 808 | 809 | def decrypt(self, data, pad=None, padmode=None): 810 | """decrypt(data, [pad], [padmode]) -> bytes 811 | 812 | data : bytes to be encrypted 813 | pad : Optional argument for decryption padding. Must only be one byte 814 | padmode : Optional argument for overriding the padding mode. 815 | 816 | The data must be a multiple of 8 bytes and will be decrypted 817 | with the already specified key. In PAD_NORMAL mode, if the 818 | optional padding character is supplied, then the un-encrypted 819 | data will have the padding characters removed from the end of 820 | the bytes. This pad removal only occurs on the last 8 bytes of 821 | the data (last data block). In PAD_PKCS5 mode, the special 822 | padding end markers will be removed from the data after 823 | decrypting, no pad character is required for PAD_PKCS5. 824 | """ 825 | ENCRYPT = des.ENCRYPT 826 | DECRYPT = des.DECRYPT 827 | data = self._guardAgainstUnicode(data) 828 | if pad is not None: 829 | pad = self._guardAgainstUnicode(pad) 830 | if self.getMode() == CBC: 831 | self.__key1.setIV(self.getIV()) 832 | self.__key2.setIV(self.getIV()) 833 | self.__key3.setIV(self.getIV()) 834 | i = 0 835 | result = [] 836 | while i < len(data): 837 | iv = data[i:i + 8] 838 | block = self.__key3.crypt(iv, DECRYPT) 839 | block = self.__key2.crypt(block, ENCRYPT) 840 | block = self.__key1.crypt(block, DECRYPT) 841 | self.__key1.setIV(iv) 842 | self.__key2.setIV(iv) 843 | self.__key3.setIV(iv) 844 | result.append(block) 845 | i += 8 846 | if _pythonMajorVersion < 3: 847 | data = ''.join(result) 848 | else: 849 | data = bytes.fromhex('').join(result) 850 | else: 851 | data = self.__key3.crypt(data, DECRYPT) 852 | data = self.__key2.crypt(data, ENCRYPT) 853 | data = self.__key1.crypt(data, DECRYPT) 854 | return self._unpadData(data, pad, padmode) 855 | -------------------------------------------------------------------------------- /hdu_api/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | hdu_api.api 5 | ----------- 6 | 7 | This module implements the hdu_api API. 8 | """ 9 | 10 | from hdu_api.models import Card, Exam, Person, Course, Public 11 | from hdu_api.sessions import SessionManager 12 | 13 | 14 | class HDU(object): 15 | def __init__(self, username, password, **kwargs): 16 | self.username = username 17 | self.password = password 18 | 19 | def __str__(self): 20 | return "