<p align="center">
<h1 align="center">UnqDantic</h1>
<p align="center">基于 UnQLite 的嵌入式数据库 ODM</p>
</p>
<p align="center">
<a href="./LICENSE">
<img src="https://img.shields.io/github/license/CMHopeSunshine/unqdantic.svg" alt="license">
</a>
<a href="https://pypi.python.org/pypi/unqdantic">
<img src="https://img.shields.io/pypi/v/unqdantic.svg" alt="pypi">
</a>
<a href="https://www.python.org/">
<img src="https://img.shields.io/badge/python-3.8+-blue.svg" alt="python">
</a>
</p>
## 简介
UnqDantic 是一个基于 [UnQLite](https://github.com/symisc/unqlite) 和 [Pydantic](https://pydantic-docs.helpmanual.io/) 的 Python 对象文档映射器(ODM)。
基于 UnQLite 的轻量、嵌入式特性,你可以在单文件或者内存中使用 NoSQL 数据库,就像 mongodb 一样!
得益于 Pydantic 的模型和数据验证,你可以轻松的构建一个文档模型,非常简单高效地创建和查询数据!
在简单的场景下,它完全可以替代复杂的 mongodb,让你更加高效方便地存储 JSON 文档数据。
**不要再用 json 文件当数据库用啦,来试试 UnqDantic 吧!!**
> 但是 UnQLite 不支持索引、唯一约束等特性,尚未知性能如何,可能不适用于大型项目
## 安装
> 注意,本库还在开发中,后续可能会有 breaking change,谨慎使用
- 使用 pip: `pip install unqdantic`
- 使用 Poetry: `poetry add unqdantic`
- 使用 PDM: `pdm add unqdantic`
## 示例
```python
from typing import Any, Dict, List, Optional
from unqdantic import Database, Document
from pydantic import Field, BaseModel
# 像pydantic一样定义模型,可以使用pydantic的所有特性
# 定义内嵌模型
class UserInfo(BaseModel):
money: int = 100
level: int = 1
# 定义文档模型
class User(Document):
name: str
age: int
info: UserInfo = Field(default_factory=UserInfo)
class Meta:
name: str = "user" # 指定文档的集合名,否则默认为类名的小写
db: Database # 可传入要绑定的数据库对象,或者在数据库初始化时传入本模型来绑定
# db = Database(filename=":mem:", documents=[User])
by_alias: bool = False # 是否在数据库集合中使用字段别名,同pydantic
# 初始化unqlite数据库
# filename中,:mem:代表在内存中使用,也可以传入文件路径
# documents为要绑定的文档模型,数据库会自动为模型创建同名的集合
db = Database(filename=":mem:", documents=[User])
# db = Database(filename=pathlib.Path("my_data.db"), documents=[User])
# 使用Pydantic式创建文档对象,调用insert()来插入文档
user1 = User(name="a", age=15).insert()
# 更新模型对象的属性,并更新到数据库
user1.update(
fields={
User.age: 20,
User.info.money: 150,
},
)
# 也可以用关键字参数形式更新
user1.update(age=20)
# 还可以手动修改后,调用save()来更新到数据库
user1.info.level = 2
user1.save()
# 如果没有name为a且age为15的文档,则创建,否则更新info.level为2
user2 = User.update_or_create(
User.name == "a",
User.age == 20,
defaults={
User.info.level: 2,
},
)
# 这两个模型为同一个文档,id一致
assert user1.id == user2.id
# 删除该文档
user2.delete()
# 可以先创建模型对象,然后使用save_all批量插入到数据库
user3 = User(name="b", age=18, info=UserInfo(money=150))
user4 = User(name="c", age=25, info=UserInfo(money=200, level=20))
User.save_all(user3, user4)
# 如果没有name为d的文档,则使用defaults中的数据创建它,否则获取它
user5 = User.get_or_create(
User.name == "d",
defaults={
User.age: 15,
User.info.level: 10,
},
)
# 根据主键id查询文档
user: Optional[User] = User.get_by_id(id=0)
# 根据主键id删除文档
delete_result: bool = User.delete_by_id(id=0)
# 查询满足所有条件的文档
users: List[User] = User.find_all(User.age == 18, User.info.money >= 150)
# 查询满足任一条件的文档
users: List[User] = User.find_all((User.age <= 18) | (User.info.level >= 2))
# 查询满足条件的首个文档,如无则返回None
user: Optional[User] = User.find_one(User.age >= 18)
# 取出所有文档
all_users: List[User] = User.all()
# 导出所有文档为dict对象
user_dicts: List[Dict[str, Any]] = User.export_all_to_dict()
# 从Dict对象列表批量保存文档
users: List[User] = User.bulk_save_from_dict(user_dicts)
# 清空所有文档
User.clear()
# 如果你不想使用文档模型,也可以直接操作集合
from unqdantic import Collection
# 需要传入数据库对象和集合名
collection = Collection(db=db, name="custom_collection")
# 可以存放任意dict或List[dict],但是要注意:
# 值只支持str、int、float、bool等基本数据类型,复杂类型可能会被存为None
# 返回结果为最后一个文档的主键id
id: int = collection.store({"key": "value"})
id: int = collection.store([{"name": "a"}, {"name": "b", "age": 18}])
# 获取指定id的文档
data: Optional[Dict[str, Any]] = collection.fetch(id=id)
# 更新指定id的文档
# 注意,它不等于dict.update,它是完全替换旧的文档内容
collection.update(id=id, data={"key": "value2"})
# 删除文档
collection.delete(id=id)
# 可以过滤查询文档
# 需要传入一个参数为文档,返回值为bool的函数
datas: Optional[List[Dict[str, Any]]] = collection.filter(lambda doc: doc["name"] == "a")
# 除此之外,你还可以直接将数据库db当键值对数据库使用,就像python的dict一样
# 但是同样的,值只支持基本数据类型,并且返回值是该值的bytes形式
# 存一个键值对
db["key"] = "value"
# 取出值
value: Optional[bytes] = db["key"] # b"value"
# 删除值
del db["key"]
# 查看键值对是否存在
assert "key" not in db
```
## 后续计划
- [ ] 允许自定义encoder、decoder
- [ ] 复杂的事务支持
- [ ] Async IO 支持(也许不会)
## 鸣谢
- [UnQLite](https://github.com/symisc/unqlite): 本项目的基础, C 语言编写的嵌入式 NoSQL 文档数据库
- [unqlite-python](https://github.com/coleifer/unqlite-python):本项目的基础,unqlite 的 python binding
- [Pydantic](https://pydantic-docs.helpmanual.io/): 本项目的基础,数据模型检验库
- [mango](https://github.com/A-kirami/mango): odm 代码参考
Raw data
{
"_id": null,
"home_page": "https://github.com/CMHopeSunshine/unqdantic",
"name": "unqdantic",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.8,<4.0",
"maintainer_email": "",
"keywords": "",
"author": "CMHopeSunshine",
"author_email": "277073121@qq.com",
"download_url": "https://files.pythonhosted.org/packages/43/31/f0cd0ef8b557d32b1193b38e2ee55a7b29bd276ff41029486d826e002cc7/unqdantic-0.2.0.tar.gz",
"platform": null,
"description": "<p align=\"center\">\n <h1 align=\"center\">UnqDantic</h1>\n <p align=\"center\">\u57fa\u4e8e UnQLite \u7684\u5d4c\u5165\u5f0f\u6570\u636e\u5e93 ODM</p>\n</p>\n<p align=\"center\">\n <a href=\"./LICENSE\">\n <img src=\"https://img.shields.io/github/license/CMHopeSunshine/unqdantic.svg\" alt=\"license\">\n </a>\n <a href=\"https://pypi.python.org/pypi/unqdantic\">\n <img src=\"https://img.shields.io/pypi/v/unqdantic.svg\" alt=\"pypi\">\n </a>\n <a href=\"https://www.python.org/\">\n <img src=\"https://img.shields.io/badge/python-3.8+-blue.svg\" alt=\"python\">\n </a>\n</p>\n\n## \u7b80\u4ecb\n\nUnqDantic \u662f\u4e00\u4e2a\u57fa\u4e8e [UnQLite](https://github.com/symisc/unqlite) \u548c [Pydantic](https://pydantic-docs.helpmanual.io/) \u7684 Python \u5bf9\u8c61\u6587\u6863\u6620\u5c04\u5668(ODM)\u3002\n\n\u57fa\u4e8e UnQLite \u7684\u8f7b\u91cf\u3001\u5d4c\u5165\u5f0f\u7279\u6027\uff0c\u4f60\u53ef\u4ee5\u5728\u5355\u6587\u4ef6\u6216\u8005\u5185\u5b58\u4e2d\u4f7f\u7528 NoSQL \u6570\u636e\u5e93\uff0c\u5c31\u50cf mongodb \u4e00\u6837\uff01\n\n\u5f97\u76ca\u4e8e Pydantic \u7684\u6a21\u578b\u548c\u6570\u636e\u9a8c\u8bc1\uff0c\u4f60\u53ef\u4ee5\u8f7b\u677e\u7684\u6784\u5efa\u4e00\u4e2a\u6587\u6863\u6a21\u578b\uff0c\u975e\u5e38\u7b80\u5355\u9ad8\u6548\u5730\u521b\u5efa\u548c\u67e5\u8be2\u6570\u636e\uff01\n\n\u5728\u7b80\u5355\u7684\u573a\u666f\u4e0b\uff0c\u5b83\u5b8c\u5168\u53ef\u4ee5\u66ff\u4ee3\u590d\u6742\u7684 mongodb\uff0c\u8ba9\u4f60\u66f4\u52a0\u9ad8\u6548\u65b9\u4fbf\u5730\u5b58\u50a8 JSON \u6587\u6863\u6570\u636e\u3002\n\n**\u4e0d\u8981\u518d\u7528 json \u6587\u4ef6\u5f53\u6570\u636e\u5e93\u7528\u5566\uff0c\u6765\u8bd5\u8bd5 UnqDantic \u5427\uff01\uff01**\n\n> \u4f46\u662f UnQLite \u4e0d\u652f\u6301\u7d22\u5f15\u3001\u552f\u4e00\u7ea6\u675f\u7b49\u7279\u6027\uff0c\u5c1a\u672a\u77e5\u6027\u80fd\u5982\u4f55\uff0c\u53ef\u80fd\u4e0d\u9002\u7528\u4e8e\u5927\u578b\u9879\u76ee\n\n## \u5b89\u88c5\n\n> \u6ce8\u610f\uff0c\u672c\u5e93\u8fd8\u5728\u5f00\u53d1\u4e2d\uff0c\u540e\u7eed\u53ef\u80fd\u4f1a\u6709 breaking change\uff0c\u8c28\u614e\u4f7f\u7528\n\n- \u4f7f\u7528 pip: `pip install unqdantic`\n- \u4f7f\u7528 Poetry: `poetry add unqdantic`\n- \u4f7f\u7528 PDM: `pdm add unqdantic`\n\n## \u793a\u4f8b\n\n```python\nfrom typing import Any, Dict, List, Optional\n\nfrom unqdantic import Database, Document\n\nfrom pydantic import Field, BaseModel\n\n\n# \u50cfpydantic\u4e00\u6837\u5b9a\u4e49\u6a21\u578b\uff0c\u53ef\u4ee5\u4f7f\u7528pydantic\u7684\u6240\u6709\u7279\u6027\n# \u5b9a\u4e49\u5185\u5d4c\u6a21\u578b\nclass UserInfo(BaseModel):\n money: int = 100\n level: int = 1\n\n\n# \u5b9a\u4e49\u6587\u6863\u6a21\u578b\nclass User(Document):\n name: str\n age: int\n info: UserInfo = Field(default_factory=UserInfo)\n\n class Meta:\n name: str = \"user\" # \u6307\u5b9a\u6587\u6863\u7684\u96c6\u5408\u540d\uff0c\u5426\u5219\u9ed8\u8ba4\u4e3a\u7c7b\u540d\u7684\u5c0f\u5199\n db: Database # \u53ef\u4f20\u5165\u8981\u7ed1\u5b9a\u7684\u6570\u636e\u5e93\u5bf9\u8c61\uff0c\u6216\u8005\u5728\u6570\u636e\u5e93\u521d\u59cb\u5316\u65f6\u4f20\u5165\u672c\u6a21\u578b\u6765\u7ed1\u5b9a\n # db = Database(filename=\":mem:\", documents=[User])\n by_alias: bool = False # \u662f\u5426\u5728\u6570\u636e\u5e93\u96c6\u5408\u4e2d\u4f7f\u7528\u5b57\u6bb5\u522b\u540d\uff0c\u540cpydantic\n\n\n# \u521d\u59cb\u5316unqlite\u6570\u636e\u5e93\n# filename\u4e2d\uff0c:mem:\u4ee3\u8868\u5728\u5185\u5b58\u4e2d\u4f7f\u7528\uff0c\u4e5f\u53ef\u4ee5\u4f20\u5165\u6587\u4ef6\u8def\u5f84\n# documents\u4e3a\u8981\u7ed1\u5b9a\u7684\u6587\u6863\u6a21\u578b\uff0c\u6570\u636e\u5e93\u4f1a\u81ea\u52a8\u4e3a\u6a21\u578b\u521b\u5efa\u540c\u540d\u7684\u96c6\u5408\ndb = Database(filename=\":mem:\", documents=[User])\n# db = Database(filename=pathlib.Path(\"my_data.db\"), documents=[User])\n\n# \u4f7f\u7528Pydantic\u5f0f\u521b\u5efa\u6587\u6863\u5bf9\u8c61\uff0c\u8c03\u7528insert()\u6765\u63d2\u5165\u6587\u6863\nuser1 = User(name=\"a\", age=15).insert()\n\n# \u66f4\u65b0\u6a21\u578b\u5bf9\u8c61\u7684\u5c5e\u6027\uff0c\u5e76\u66f4\u65b0\u5230\u6570\u636e\u5e93\nuser1.update(\n fields={\n User.age: 20,\n User.info.money: 150,\n },\n)\n# \u4e5f\u53ef\u4ee5\u7528\u5173\u952e\u5b57\u53c2\u6570\u5f62\u5f0f\u66f4\u65b0\nuser1.update(age=20)\n# \u8fd8\u53ef\u4ee5\u624b\u52a8\u4fee\u6539\u540e\uff0c\u8c03\u7528save()\u6765\u66f4\u65b0\u5230\u6570\u636e\u5e93\nuser1.info.level = 2\nuser1.save()\n\n# \u5982\u679c\u6ca1\u6709name\u4e3aa\u4e14age\u4e3a15\u7684\u6587\u6863\uff0c\u5219\u521b\u5efa\uff0c\u5426\u5219\u66f4\u65b0info.level\u4e3a2\nuser2 = User.update_or_create(\n User.name == \"a\",\n User.age == 20,\n defaults={\n User.info.level: 2,\n },\n)\n# \u8fd9\u4e24\u4e2a\u6a21\u578b\u4e3a\u540c\u4e00\u4e2a\u6587\u6863\uff0cid\u4e00\u81f4\nassert user1.id == user2.id\n\n# \u5220\u9664\u8be5\u6587\u6863\nuser2.delete()\n\n# \u53ef\u4ee5\u5148\u521b\u5efa\u6a21\u578b\u5bf9\u8c61\uff0c\u7136\u540e\u4f7f\u7528save_all\u6279\u91cf\u63d2\u5165\u5230\u6570\u636e\u5e93\nuser3 = User(name=\"b\", age=18, info=UserInfo(money=150))\nuser4 = User(name=\"c\", age=25, info=UserInfo(money=200, level=20))\nUser.save_all(user3, user4)\n\n# \u5982\u679c\u6ca1\u6709name\u4e3ad\u7684\u6587\u6863\uff0c\u5219\u4f7f\u7528defaults\u4e2d\u7684\u6570\u636e\u521b\u5efa\u5b83\uff0c\u5426\u5219\u83b7\u53d6\u5b83\nuser5 = User.get_or_create(\n User.name == \"d\",\n defaults={\n User.age: 15,\n User.info.level: 10,\n },\n)\n\n# \u6839\u636e\u4e3b\u952eid\u67e5\u8be2\u6587\u6863\nuser: Optional[User] = User.get_by_id(id=0)\n# \u6839\u636e\u4e3b\u952eid\u5220\u9664\u6587\u6863\ndelete_result: bool = User.delete_by_id(id=0)\n\n# \u67e5\u8be2\u6ee1\u8db3\u6240\u6709\u6761\u4ef6\u7684\u6587\u6863\nusers: List[User] = User.find_all(User.age == 18, User.info.money >= 150)\n# \u67e5\u8be2\u6ee1\u8db3\u4efb\u4e00\u6761\u4ef6\u7684\u6587\u6863\nusers: List[User] = User.find_all((User.age <= 18) | (User.info.level >= 2))\n# \u67e5\u8be2\u6ee1\u8db3\u6761\u4ef6\u7684\u9996\u4e2a\u6587\u6863\uff0c\u5982\u65e0\u5219\u8fd4\u56deNone\nuser: Optional[User] = User.find_one(User.age >= 18)\n\n# \u53d6\u51fa\u6240\u6709\u6587\u6863\nall_users: List[User] = User.all()\n# \u5bfc\u51fa\u6240\u6709\u6587\u6863\u4e3adict\u5bf9\u8c61\nuser_dicts: List[Dict[str, Any]] = User.export_all_to_dict()\n# \u4eceDict\u5bf9\u8c61\u5217\u8868\u6279\u91cf\u4fdd\u5b58\u6587\u6863\nusers: List[User] = User.bulk_save_from_dict(user_dicts)\n# \u6e05\u7a7a\u6240\u6709\u6587\u6863\nUser.clear()\n\n\n# \u5982\u679c\u4f60\u4e0d\u60f3\u4f7f\u7528\u6587\u6863\u6a21\u578b\uff0c\u4e5f\u53ef\u4ee5\u76f4\u63a5\u64cd\u4f5c\u96c6\u5408\nfrom unqdantic import Collection\n\n# \u9700\u8981\u4f20\u5165\u6570\u636e\u5e93\u5bf9\u8c61\u548c\u96c6\u5408\u540d\ncollection = Collection(db=db, name=\"custom_collection\")\n# \u53ef\u4ee5\u5b58\u653e\u4efb\u610fdict\u6216List[dict]\uff0c\u4f46\u662f\u8981\u6ce8\u610f\uff1a\n# \u503c\u53ea\u652f\u6301str\u3001int\u3001float\u3001bool\u7b49\u57fa\u672c\u6570\u636e\u7c7b\u578b\uff0c\u590d\u6742\u7c7b\u578b\u53ef\u80fd\u4f1a\u88ab\u5b58\u4e3aNone\n# \u8fd4\u56de\u7ed3\u679c\u4e3a\u6700\u540e\u4e00\u4e2a\u6587\u6863\u7684\u4e3b\u952eid\nid: int = collection.store({\"key\": \"value\"})\nid: int = collection.store([{\"name\": \"a\"}, {\"name\": \"b\", \"age\": 18}])\n# \u83b7\u53d6\u6307\u5b9aid\u7684\u6587\u6863\ndata: Optional[Dict[str, Any]] = collection.fetch(id=id)\n# \u66f4\u65b0\u6307\u5b9aid\u7684\u6587\u6863\n# \u6ce8\u610f\uff0c\u5b83\u4e0d\u7b49\u4e8edict.update\uff0c\u5b83\u662f\u5b8c\u5168\u66ff\u6362\u65e7\u7684\u6587\u6863\u5185\u5bb9\ncollection.update(id=id, data={\"key\": \"value2\"})\n# \u5220\u9664\u6587\u6863\ncollection.delete(id=id)\n\n# \u53ef\u4ee5\u8fc7\u6ee4\u67e5\u8be2\u6587\u6863\n# \u9700\u8981\u4f20\u5165\u4e00\u4e2a\u53c2\u6570\u4e3a\u6587\u6863\uff0c\u8fd4\u56de\u503c\u4e3abool\u7684\u51fd\u6570\ndatas: Optional[List[Dict[str, Any]]] = collection.filter(lambda doc: doc[\"name\"] == \"a\")\n\n# \u9664\u6b64\u4e4b\u5916\uff0c\u4f60\u8fd8\u53ef\u4ee5\u76f4\u63a5\u5c06\u6570\u636e\u5e93db\u5f53\u952e\u503c\u5bf9\u6570\u636e\u5e93\u4f7f\u7528\uff0c\u5c31\u50cfpython\u7684dict\u4e00\u6837\n# \u4f46\u662f\u540c\u6837\u7684\uff0c\u503c\u53ea\u652f\u6301\u57fa\u672c\u6570\u636e\u7c7b\u578b\uff0c\u5e76\u4e14\u8fd4\u56de\u503c\u662f\u8be5\u503c\u7684bytes\u5f62\u5f0f\n\n# \u5b58\u4e00\u4e2a\u952e\u503c\u5bf9\ndb[\"key\"] = \"value\"\n# \u53d6\u51fa\u503c\nvalue: Optional[bytes] = db[\"key\"] # b\"value\"\n# \u5220\u9664\u503c\ndel db[\"key\"]\n# \u67e5\u770b\u952e\u503c\u5bf9\u662f\u5426\u5b58\u5728\nassert \"key\" not in db\n\n```\n\n## \u540e\u7eed\u8ba1\u5212\n\n- [ ] \u5141\u8bb8\u81ea\u5b9a\u4e49encoder\u3001decoder\n- [ ] \u590d\u6742\u7684\u4e8b\u52a1\u652f\u6301\n- [ ] Async IO \u652f\u6301(\u4e5f\u8bb8\u4e0d\u4f1a)\n\n## \u9e23\u8c22\n\n- [UnQLite](https://github.com/symisc/unqlite): \u672c\u9879\u76ee\u7684\u57fa\u7840\uff0c C \u8bed\u8a00\u7f16\u5199\u7684\u5d4c\u5165\u5f0f NoSQL \u6587\u6863\u6570\u636e\u5e93\n- [unqlite-python](https://github.com/coleifer/unqlite-python)\uff1a\u672c\u9879\u76ee\u7684\u57fa\u7840\uff0cunqlite \u7684 python binding\n- [Pydantic](https://pydantic-docs.helpmanual.io/): \u672c\u9879\u76ee\u7684\u57fa\u7840\uff0c\u6570\u636e\u6a21\u578b\u68c0\u9a8c\u5e93\n- [mango](https://github.com/A-kirami/mango): odm \u4ee3\u7801\u53c2\u8003\n\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "A embedded NoSQL database ODM based on UnQLite.",
"version": "0.2.0",
"project_urls": {
"Homepage": "https://github.com/CMHopeSunshine/unqdantic",
"Repository": "https://github.com/CMHopeSunshine/unqdantic.git"
},
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "386effb6181c8b648c20869c9eaa53dda4c1c4f33a5ec7e0406914448a6a7772",
"md5": "c5e38db97a02f95ef17b23d8d38fc4d6",
"sha256": "2cdb135194ff0945f181d0c2f82fdbe2301be040ff4680b98c2892061f910c84"
},
"downloads": -1,
"filename": "unqdantic-0.2.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "c5e38db97a02f95ef17b23d8d38fc4d6",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8,<4.0",
"size": 12354,
"upload_time": "2023-11-13T14:06:08",
"upload_time_iso_8601": "2023-11-13T14:06:08.560267Z",
"url": "https://files.pythonhosted.org/packages/38/6e/ffb6181c8b648c20869c9eaa53dda4c1c4f33a5ec7e0406914448a6a7772/unqdantic-0.2.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "4331f0cd0ef8b557d32b1193b38e2ee55a7b29bd276ff41029486d826e002cc7",
"md5": "4c74fc13d54abe7f0ad26835f39184ac",
"sha256": "9c959dd6acee2d64e78a627083f48720d9f9e8718f8138221d63359d6bc551d3"
},
"downloads": -1,
"filename": "unqdantic-0.2.0.tar.gz",
"has_sig": false,
"md5_digest": "4c74fc13d54abe7f0ad26835f39184ac",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8,<4.0",
"size": 13691,
"upload_time": "2023-11-13T14:06:10",
"upload_time_iso_8601": "2023-11-13T14:06:10.244428Z",
"url": "https://files.pythonhosted.org/packages/43/31/f0cd0ef8b557d32b1193b38e2ee55a7b29bd276ff41029486d826e002cc7/unqdantic-0.2.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-11-13 14:06:10",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "CMHopeSunshine",
"github_project": "unqdantic",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "unqdantic"
}