diff --git a/app.py b/app.py index 5509a70..4989b10 100644 --- a/app.py +++ b/app.py @@ -1,15 +1,25 @@ import sys import os.path +from logging.config import dictConfig + from flask import Flask +from flask.logging import default_handler sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) from database.mongodb import initialize_db from routes import blueprint_api +from settings.dev import LOGGING +# 日志配置 +dictConfig(LOGGING) + +# app 实例,移除默认日志,加载配置文件 app = Flask(__name__) +app.logger.removeHandler(default_handler) app.config.from_pyfile("settings/dev.py") +# 初始化数据库连接 initialize_db(app) # 注册蓝图 diff --git a/src/game/__init__.py b/scripts/__init__.py similarity index 100% rename from src/game/__init__.py rename to scripts/__init__.py diff --git a/scripts/insert_test_data.py b/scripts/insert_test_data.py new file mode 100644 index 0000000..c4022b9 --- /dev/null +++ b/scripts/insert_test_data.py @@ -0,0 +1,132 @@ +from asset.models import Host +from project.models import Project, Channel, Server, Version +import logging + + +logger = logging.getLogger('flask.app.script') + +projects = [ + { + "name": "ts01/cn", + "id": 1, + "desc": "测试", + "domain": "www.baidu.com", + "www_ip": "127.0.0.1", + "ops_ip": "10.2.2.10", + "transfer_ip": "10.2.2.10", + "webhook": "", + "kw": "" + }, + { + "name": "ab01/cn", + "id": 2, + "desc": "测试ab01", + "domain": "ab01.www.baidu.com", + "www_ip": "127.0.0.1", + "ops_ip": "10.2.2.11", + "transfer_ip": "10.2.2.13", + "webhook": "", + "kw": "" + }, + { + "name": "aa11/tw", + "id": 3, + "desc": "1212", + "domain": "www.baidu.com", + "www_ip": "127.0.0.1", + "ops_ip": "11.22.33.44", + "transfer_ip": "11.22.33.45", + "webhook": "", + "kw": "" + }, + { + "name": "qq88/cn", + "id": 4, + "desc": "qq88/cn", + "domain": "qq88.com", + "www_ip": "127.0.0.1", + "ops_ip": "10.2.2.10", + "transfer_ip": "10.2.2.11", + "webhook": "", + "kw": "" + }, + { + "name": "ug41/cn", + "id": 5, + "desc": "ug41", + "domain": "ug41.huanyuantech.com", + "www_ip": "127.0.0.1", + "ops_ip": "127.0.0.1", + "transfer_ip": "127.0.0.1", + "webhook": "", + "kw": "ug41-cn" + } +] + + +# def sync_project(): +# for p in projects: +# proj = p +# name = proj.pop("name") +# game, fork = name.split("/") +# proj["name"] = game +# proj["fork"] = fork +# proj.pop("id") +# # 字典中缺少的值会是None,字段必须null=True +# obj = models.Project(**proj) +# obj.save() + + +def sync_server_info(): + project = Project.objects(name="ug41", fork="cn").first() + info = [{"ip": "10.2.2.10", "spid": "abo", "num": 1, "server_version": "2021100801", "cfg_version": "8616", "admin_version": "2021062801", "lua_version": "122", "flag": "2", "status": "0", "port": 0}, {"ip": "10.2.2.11", "spid": "dev", "num": 1, "server_version": "2021100801", "cfg_version": "7950", "admin_version": "2021062801", "lua_version": "122", "flag": "0", "status": "1", "port": 0}, {"ip": "10.2.2.11", "spid": "dev", "num": 2, "server_version": "2021100801", "cfg_version": "2014", "admin_version": "2021062801", "lua_version": "122", "flag": "0", "status": "1", "port": 0}, {"ip": "10.2.2.11", "spid": "dev", "num": 3, "server_version": "2021100801", "cfg_version": "7003", "admin_version": "2021062801", "lua_version": "122", "flag": "0", "status": "1", "port": 0}, {"ip": "10.2.2.11", "spid": "dev", "num": 4, "server_version": "2021100801", "cfg_version": "8406", "admin_version": "2021062801", "lua_version": "122", "flag": "0", "status": "1", "port": 0}, {"ip": "10.2.2.11", "spid": "dev", "num": 5, "server_version": "2021100801", "cfg_version": "17399", "admin_version": "2021062801", "lua_version": "122", "flag": "0", "status": "1", "port": 0}, {"ip": "10.2.2.11", "spid": "dev", "num": 6, "server_version": "2021100801", "cfg_version": "8375", "admin_version": "2021062801", "lua_version": "122", "flag": "0", "status": "1", "port": 0}] + for srv in info: + ip = srv.get("ip") + num = srv.get("num") + spid = srv.get("spid") + port = srv.get("port", 0) + # 内嵌的版本字段 + version = Version() + version.admin = srv.get("admin_version", "") + version.server = srv.get("server_version", "") + version.config = srv.get("cfg_version", "") + version.lua = srv.get("lua_version", "") + version.bin = srv.get("bin_version", "") + version.sql = srv.get("sql_version", "") + state = srv.get("status", "1") + if srv.get("flag", "0") == "2": + status = "closed" + else: + status = "running" if state == "1" else "error" + + try: + host = Host.objects(public_ip=ip).first() + if not host: + host = Host(public_ip=ip) + host.save() + + channel = Channel.objects(project=project, spid=spid).first() + if not channel: + channel = Channel(project=project, spid=spid) + channel.save() + + # 更新、创建的参数 + defaults = dict(host=host, version=version, port=port, status=status) + srv_obj = Server.objects(num=num, channel=channel).first() + if not srv_obj: # 创建 + srv_obj = Server(num=num, channel=channel, **defaults) + srv_obj.save() + logger.info(f"创建 {spid}_{num} 区服信息成功") + continue + # 更新对象 + srv_obj.update(**defaults) + srv_obj.save() + logger.info(f"更新 {spid}_{num} 区服信息成功") + except: + logger.exception("同步区服出错") + continue + + +if __name__ == '__main__': + # sync_project() + sync_server_info() diff --git a/settings/dev.py b/settings/dev.py index 83d1098..4689602 100644 --- a/settings/dev.py +++ b/settings/dev.py @@ -1,5 +1,47 @@ +import os.path + +# 项目目录 +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # mongodb 地址 MONGODB_SETTINGS = { 'host': 'mongodb://admin:111111@10.2.2.10:27017/ops_api?authSource=admin', } + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': True, + 'formatters': { + 'verbose': { + 'format': '%(asctime)s %(levelname)s %(module)s:%(funcName)s %(process)d %(thread)d %(message)s' + }, + 'simple': { + 'format': '%(levelname)s %(message)s' + }, + }, + 'filters': { + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'simple' + }, + 'file_app': { + 'level': 'DEBUG', + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': os.path.join(os.path.dirname(BASE_DIR), 'logs/app.log'), + 'encoding': 'utf-8', + 'maxBytes': 1024 * 1024 * 5, + 'backupCount': 5, + 'formatter': 'verbose' + }, + }, + 'loggers': { + 'root': { + 'handlers': ['file_app'], + 'level': 'DEBUG', + 'propagate': True, + }, + } +} diff --git a/src/asset/__init__.py b/src/asset/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/asset/fields.py b/src/asset/fields.py new file mode 100644 index 0000000..4f18f2e --- /dev/null +++ b/src/asset/fields.py @@ -0,0 +1,26 @@ +""" +asset marshal fields +""" + +from flask_restful import fields + + +HostFields = { + "id": fields.String, + "public_ip": fields.String, + "private_ip": fields.String, + "minion_id": fields.String, + "weights": fields.Integer, + "cpu_num": fields.Integer, + "cpu_core": fields.Integer, + "memory": fields.Integer, + "tags": fields.List(fields.String), + "labels": fields.Raw, + "created": fields.DateTime, +} + +HostSimpleFields = { + "id": fields.String, + "public_ip": fields.String, + "private_ip": fields.String, +} diff --git a/src/asset/models.py b/src/asset/models.py new file mode 100644 index 0000000..a6cfcbf --- /dev/null +++ b/src/asset/models.py @@ -0,0 +1,28 @@ +import mongoengine as mongo + +from common.document import DocumentBase +from common.validator import is_ipaddr + + +class Host(DocumentBase): + + # STATUS = { + # "no": "" + # } + + public_ip = mongo.StringField(max_length=64, required=True, unique=True, validation=is_ipaddr) + private_ip = mongo.StringField(max_length=64, default="") + minion_id = mongo.StringField(max_length=64, default="") # 需要唯一,先留空 + weights = mongo.IntField(default=40) + # status = mongo.StringField() + # spec = mongo.EmbeddedDocumentField(Spec) + + cpu_num = mongo.IntField(default=1) # cpu物理个数 + cpu_core = mongo.IntField(default=1) # 每个cpu的核心数 + memory = mongo.IntField(default=0) # 内存大小,单位GB + + # 标记和标签 + tags = mongo.ListField(mongo.StringField(), default=list) # tags 默认是空列表 + labels = mongo.DictField(default=dict) + + created = mongo.DateTimeField() diff --git a/src/asset/routes.py b/src/asset/routes.py new file mode 100644 index 0000000..b44746c --- /dev/null +++ b/src/asset/routes.py @@ -0,0 +1,13 @@ +from flask import Blueprint +from flask_restful import Api + +from asset import views + + +# 当前app的蓝图,以app名为前缀 +asset = Blueprint('asset', __name__, url_prefix="/asset") + +# 增加路由 +api = Api(asset) +api.add_resource(views.HostViews, '/host/', endpoint="host") +api.add_resource(views.HostDetailViews, '/host//', endpoint="host-detail") diff --git a/src/asset/views.py b/src/asset/views.py new file mode 100644 index 0000000..45a0321 --- /dev/null +++ b/src/asset/views.py @@ -0,0 +1,14 @@ +from asset import fields +from asset.models import Host + +from common.views import ListCreateViewSet, DetailViewSet + + +class HostViews(ListCreateViewSet): + model = Host + fields = fields.HostFields + + +class HostDetailViews(DetailViewSet): + model = Host + fields = fields.HostFields diff --git a/src/common/document.py b/src/common/document.py new file mode 100644 index 0000000..757059d --- /dev/null +++ b/src/common/document.py @@ -0,0 +1,44 @@ +""" +mongo 文档相关封装 +""" + +from flask_mongoengine import Document, BaseQuerySet + + +class DocumentBase(Document): + """自定义的文档处理方法扩展 + 注意此类必须设置meta如下,参考 `flask_mongoengine.Document`: + {"abstract": True, "queryset_class": BaseQuerySet} + """ + + meta = {"abstract": True, "queryset_class": BaseQuerySet} + + @classmethod + def get_or_create(cls, defaults: dict = None, **kwargs): + """获取或创建一个对象,必须传好参数,确保正确且唯一,需要外部捕捉异常 + :param defaults: 创建参数 + :param kwargs: 对象查找参数 + """ + created = False + obj = cls.objects(**kwargs).first() + if not obj: + obj = cls(**defaults) + obj.save() + created = True + return obj, created + + @classmethod + def update_or_create(cls, defaults: dict = None, **kwargs): + """更新或创建对象 + :param defaults: 创建、更新参数 + :param kwargs: 对象查找参数 + """ + created = False + obj = cls.objects(**kwargs).first() + if not obj: + obj = cls(**defaults) + obj.save() + created = True + else: + obj.update(**defaults) + return obj, created diff --git a/src/common/validator.py b/src/common/validator.py new file mode 100644 index 0000000..7279a33 --- /dev/null +++ b/src/common/validator.py @@ -0,0 +1,15 @@ +import ipaddress +import mongoengine as mongo + + +def isalnum(string: str): + """数字、字母验证器""" + if not string.isalnum(): + raise mongo.ValidationError('The value must be a combination of letters and numbers') + + +def is_ipaddr(ip): + try: + ipaddress.ip_address(ip) + except: + raise mongo.ValidationError('The value must be a ip address') diff --git a/src/common/views.py b/src/common/views.py index 03d2269..bbe0e5e 100644 --- a/src/common/views.py +++ b/src/common/views.py @@ -1,14 +1,14 @@ -from flask_restful import abort, Resource, marshal, fields +from flask_restful import abort, Resource, marshal, fields, reqparse class ModelViewBase(Resource): model = None fields = {} - request_parse = None + request_parse: reqparse.RequestParser = None def get_object(self, pk): try: - return self.model.objects.get_or_404(id=pk) + return self.model.objects(id=pk).first_or_404(message="resource not found") except: abort(404, msg=f"resource '{pk}' not found") diff --git a/src/game/models.py b/src/game/models.py deleted file mode 100644 index 139597f..0000000 --- a/src/game/models.py +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/src/game/routes.py b/src/game/routes.py deleted file mode 100644 index bf66d4c..0000000 --- a/src/game/routes.py +++ /dev/null @@ -1,11 +0,0 @@ -from game.views import Server -from flask import Blueprint -from flask_restful import Api - -# 当前app的蓝图,以app名为前缀 -game = Blueprint('game', __name__, url_prefix="/game") - -# 增加路由 -api = Api(game) -api.add_resource(Server, '/server/', endpoint="server") - diff --git a/src/game/views.py b/src/game/views.py deleted file mode 100644 index 6558ca3..0000000 --- a/src/game/views.py +++ /dev/null @@ -1,12 +0,0 @@ -from flask_restful import Resource - - -class Server(Resource): - """""" - def get(self): - return {"msg": "ok", "method": "get"} - - def post(self): - return {"msg": "ok", "method": "post"} - - diff --git a/src/project/__init__.py b/src/project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/project/fields.py b/src/project/fields.py new file mode 100644 index 0000000..2bf3761 --- /dev/null +++ b/src/project/fields.py @@ -0,0 +1,71 @@ +""" +project marshal fields +""" + +from flask_restful import fields +from asset.fields import HostSimpleFields + +ProjectFields = { + "id": fields.String, + "name": fields.String, + "fork": fields.String, + "domain": fields.String, + "www_ip": fields.String, + "ops_ip": fields.String, + "transfer_ip": fields.String, + "webhook": fields.String, + "kw": fields.String, + "desc": fields.String, + "repository": fields.String, + "tags": fields.List(fields.String), + "labels": fields.Raw, +} + +ProjectSimpleField = { + "id": fields.String, + "name": fields.String, + "fork": fields.String, + "domain": fields.String, +} + +VersionFields = { + "admin": fields.String, + "server": fields.String, + "config": fields.String, + "lua": fields.String, + "sql": fields.String, + "bin": fields.String, +} + +ChannelFields = { + "id": fields.String, + "project": fields.Nested(ProjectSimpleField), + "name": fields.String, + "spid": fields.String, + "version": fields.Nested(VersionFields), + "repository": fields.String, + "branch": fields.String, + "tags": fields.List(fields.String), # List 必须指明类型 + "labels": fields.Raw, +} + +ChannelSimpleFields = { + "id": fields.String, + "spid": fields.String, + "project": fields.Nested(ProjectSimpleField), +} + +ServerFields = { + "id": fields.String, + "num": fields.Integer, + "channel": fields.Nested(ChannelSimpleFields), + "status": fields.String, + "host": fields.Nested(HostSimpleFields), + "domain": fields.String, + "port": fields.Integer, + "version": fields.Nested(VersionFields), + "weight": fields.Integer, + "slot": fields.Integer, + "tag": fields.List(fields.String), + "labels": fields.Raw, +} diff --git a/src/project/models.py b/src/project/models.py new file mode 100644 index 0000000..b3744e2 --- /dev/null +++ b/src/project/models.py @@ -0,0 +1,88 @@ +import mongoengine as mongo +from common.document import DocumentBase + +from common.validator import isalnum + +from asset.models import Host + + +class Project(DocumentBase): + """项目模型定义""" + # 四位字符,字母、数字组合,name、fork组合必须是唯一 + name = mongo.StringField(max_length=4, min_length=4, required=True, unique_with="fork", validation=isalnum) + fork = mongo.StringField(max_length=2, min_length=2, required=True, validation=isalnum) + domain = mongo.StringField(max_length=128, required=False) + www_ip = mongo.StringField(max_length=64, required=False) + ops_ip = mongo.StringField(max_length=64, required=False) + transfer_ip = mongo.StringField(max_length=64, required=False) + webhook = mongo.StringField(max_length=256, required=False) + kw = mongo.StringField(max_length=32, required=False) + desc = mongo.StringField(max_length=256, default="") + # 项目的代码仓库地址 + repository = mongo.StringField("仓库", max_length=256, default="", help_text="仓库地址", null=True) + # 标记和标签 + tags = mongo.ListField(mongo.StringField(), default=list) # tags 默认是空列表 + labels = mongo.DictField(default=dict) + + def fullname(self): + return f"{self.name}/{self.fork}" + + +class Version(mongo.EmbeddedDocument): + """版本信息""" + admin = mongo.StringField(max_length=32, required=False, default="") + + server = mongo.StringField(max_length=32, required=False, default="") + config = mongo.StringField(max_length=32, required=False, default="") + lua = mongo.StringField(max_length=32, required=False, default="") + sql = mongo.StringField(max_length=32, required=False, default="") + bin = mongo.StringField(max_length=32, required=False, default="") + + +class Channel(DocumentBase): + """渠道""" + # spid项目内唯一 + project = mongo.ReferenceField(Project, reverse_delete_rule=mongo.NULLIFY) + name = mongo.StringField(max_length=32, default="") + spid = mongo.StringField(max_length=3, min_length=3, required=True, unique_with="project", validation=isalnum) + version = mongo.EmbeddedDocumentField(Version) # 缺失时获取对象的此字段为 None + + # 项目的代码仓库地址 + repository = mongo.StringField(max_length=256, default="") + branch = mongo.StringField(max_length=32) + + # 标记和标签 + tags = mongo.ListField(mongo.StringField(), default=list) # tags 默认是空列表 + labels = mongo.DictField(default=dict) + + created = mongo.DateTimeField() + + +class Server(DocumentBase): + """服务""" + + STATUS = { + "prepare": "预备", + "stopped": "停止", + "running": "运行中", + "closed": "关服", + "maintain": "维护", + "error": "异常" + } + + num = mongo.IntField(required=True, unique_with="channel") + channel = mongo.ReferenceField(Channel) + status = mongo.StringField(max_length=12, choices=STATUS.keys(), required=True, default="running") + + # 机器字段,TODO 先允许为空 + host = mongo.ReferenceField(Host, null=True) + domain = mongo.StringField(max_length=128, required=False) + port = mongo.IntField() + version = mongo.EmbeddedDocumentField(Version) + weight = mongo.IntField(default=1) + slot = mongo.IntField(default=0) + + # 标记和标签 + tags = mongo.ListField(mongo.StringField(), default=list) # tags 默认是空列表 + labels = mongo.DictField(default=dict) + diff --git a/src/project/routes.py b/src/project/routes.py new file mode 100644 index 0000000..1af8185 --- /dev/null +++ b/src/project/routes.py @@ -0,0 +1,24 @@ +from flask import Blueprint +from flask_restful import Api + +from project import views + + +# 当前app的蓝图,以app名为前缀 +project = Blueprint('project', __name__, url_prefix="/project") + +# 增加路由 +api = Api(project) +# 项目模型的视图 +api.add_resource(views.ProjectViews, '/item/', endpoint="project") +api.add_resource(views.ProjectDetailViews, '/item//', endpoint="project-detail") + +# 渠道模型视图 +api.add_resource(views.ChannelViews, '/channel/', endpoint="channel") +api.add_resource(views.ChannelDetailViews, '/channel//', endpoint="channel-detail") + +# 区服模型视图 +api.add_resource(views.ServerViews, '/server/', endpoint="server") +api.add_resource(views.ServerDetailView, '/server//', endpoint="server-detail") +# 区服信息同步接口,更新区服信息 +api.add_resource(views.ServerSyncView, '/server/sync/', endpoint="server-sync") diff --git a/src/project/views.py b/src/project/views.py new file mode 100644 index 0000000..4ac7e8a --- /dev/null +++ b/src/project/views.py @@ -0,0 +1,137 @@ +import datetime + +from flask import current_app as app +from flask_restful import reqparse, abort + +from asset.models import Host +from project import fields +from project.models import Project, Channel, Server, Version +from common.views import ListMixin, CreateMixin, ListCreateViewSet, DetailViewSet + + +class ProjectViews(ListMixin, CreateMixin): + model = Project + fields = fields.ProjectFields + + +class ProjectDetailViews(DetailViewSet): + model = Project + fields = fields.ProjectFields + + +class ChannelViews(ListCreateViewSet): + model = Channel + fields = fields.ChannelFields + + +class ChannelDetailViews(DetailViewSet): + model = Channel + fields = fields.ChannelFields + + +class ServerViews(ListCreateViewSet): + model = Server + fields = fields.ServerFields + + +class ServerDetailView(DetailViewSet): + model = Server + fields = fields.ServerFields + + +class ServerSyncView(CreateMixin): + """同步区服信息,由 `batchQuery` 程序发送采集内容,此视图更新保存""" + model = Server + fields = fields.ServerFields + project: Project = None + + def __init__(self): + # 参数定义,json来源 + self.request_parse = reqparse.RequestParser() + self.request_parse.add_argument("project", type=str, required=True, location='json') + self.request_parse.add_argument("fork", type=str, required=True, location='json') + self.request_parse.add_argument("count", type=int, required=True, location='json') + self.request_parse.add_argument("data", type=list, required=True, location='json') + + def get_project(self, args): + proj = args["project"] + fork = args["fork"] + self.project = Project.objects(name=proj, fork=fork).first_or_404(message="project not found") + + @classmethod + def parse_version(cls, srv: dict): + """解析版本""" + version = Version() + version.admin = srv.get("admin_version", "") + version.server = srv.get("server_version", "") + version.config = srv.get("cfg_version", "") + version.lua = srv.get("lua_version", "") + version.bin = srv.get("bin_version", "") + version.sql = srv.get("sql_version", "") + return version + + @classmethod + def parse_status(cls, srv: dict): + """解析 flag 和 status 值,据此判断返回区服的状态""" + state = srv.get("status", "1") + if srv.get("flag", "0") == "2": + status = "closed" + else: + status = "running" if state == "1" else "error" + return status + + def post(self): + """同步区服信息,接收 json 数据""" + # 校验参数 + args = self.request_parse.parse_args() + self.get_project(args) + + count = args["count"] + data = args["data"] + + if count != len(data): + abort(400, msg="quantity mismatch", code=1001) + + hosts = {} + channels = {} + errors = [] + for srv in data: + ip = srv.get("ip") + num = srv.get("num") + spid = srv.get("spid") + port = srv.get("port", 0) + # 内嵌的版本字段 + version = self.parse_version(srv) + status = self.parse_status(srv) + host = hosts.get(ip) + channel = channels.get(spid) + + try: + if not host: + hostObj, _ = Host.get_or_create( + public_ip=ip, defaults=dict(public_ip=ip, created=datetime.datetime.now())) + host = hostObj.id + hosts[ip] = hostObj.id + if not channel: + channelObj, _ = Channel.get_or_create( + project=self.project, spid=spid, defaults=dict(project=self.project, spid=spid)) + channel = channelObj.id + channels[spid] = channelObj.id + + # 更新、创建的参数 + defaults = dict(num=num, channel=channel, host=host, version=version, port=port, status=status) + obj, created = Server.update_or_create(defaults=defaults, num=num, channel=channel) + if created: # 创建 + app.logger.debug(f"创建 {spid}_{num} 区服信息成功") + continue + # 更新对象日志 + app.logger.debug(f"更新 {spid}_{num} 区服信息成功") + except: + app.logger.exception("同步区服出错") + errors.append(f"{spid}_s{num}") + continue + + if errors: + return {"msg": "有区服同步出错!", "code": 1020, "errors": errors} + + return {"msg": "ok", "code": 1000} diff --git a/src/routes.py b/src/routes.py index 5bcdacf..5031ee9 100644 --- a/src/routes.py +++ b/src/routes.py @@ -1,9 +1,10 @@ from flask import Blueprint -from game.routes import game +from project.routes import project +from asset.routes import asset blueprint_api = Blueprint('api-main', __name__, url_prefix='/api') # 注册子蓝图,嵌套 -blueprint_api.register_blueprint(game) - +blueprint_api.register_blueprint(asset) +blueprint_api.register_blueprint(project)