개정판 caa75f02
issue #1366: upload project, model, training controller
Change-Id: Ic8f9c8a798fd9e7ce4ced12c36bba11a0b38f854
DTI_PID/WebServer/app/__init__.py | ||
---|---|---|
5 | 5 |
from flask_restx import Api, fields, marshal_with, Resource, reqparse |
6 | 6 |
from app.recognition.index import recognition_service |
7 | 7 |
from app.training.index import training_service |
8 |
from app.api.controller.license_controller import api as license_ns |
|
9 |
from app.api.controller.license_controller import license_service |
|
10 |
from app.api.controller.TrainingController import api as training_ns |
|
11 |
from app.api.controller.TrainingController import TrainingModel |
|
8 |
from app.api.controllers.ProjectController import api as project_ns |
|
9 |
from app.api.controllers.ModelController import api as model_ns |
|
10 |
from app.api.controllers.license_controller import api as license_ns |
|
11 |
from app.api.controllers.license_controller import license_service |
|
12 |
from app.api.controllers.TrainingController import api as training_ns |
|
13 |
from app.api.controllers.RecognitionController import api as recognition_ns |
|
12 | 14 |
from app.api.models import LicenseDTO |
13 | 15 |
|
14 | 16 |
|
... | ... | |
40 | 42 |
version='1.0', |
41 | 43 |
description='flask restx web service for ID2' |
42 | 44 |
) |
45 |
api.add_namespace(project_ns, path='/projects') |
|
46 |
api.add_namespace(model_ns, path='/models') |
|
43 | 47 |
api.add_namespace(license_ns, path='/license') |
44 | 48 |
api.add_namespace(training_ns, path='/training') |
49 |
api.add_namespace(recognition_ns, path='/recognition') |
DTI_PID/WebServer/app/api/controller/TrainingController.py | ||
---|---|---|
1 |
import os |
|
2 |
import zipfile |
|
3 |
from flask import request, Blueprint, redirect, url_for, render_template, make_response, jsonify |
|
4 |
from flask_restx import Namespace, Resource, reqparse |
|
5 |
from werkzeug.datastructures import FileStorage |
|
6 |
|
|
7 |
api = Namespace('training', description='file related operations') |
|
8 |
upload_parser = api.parser() |
|
9 |
upload_parser.add_argument('project_name', type=str, required=True) |
|
10 |
upload_parser.add_argument('class_file', location='files', type=FileStorage, required=True) |
|
11 |
upload_parser.add_argument('training_file', location='files', type=FileStorage, required=True) |
|
12 |
|
|
13 |
training_parser = api.parser() |
|
14 |
training_parser.add_argument('project_name', type=str, required=True) |
|
15 |
training_parser.add_argument('class_name_file', location='files', type=FileStorage, required=True) |
|
16 |
|
|
17 |
|
|
18 |
def allowed_file(filename): |
|
19 |
return os.path.splitext(filename)[1].upper() == '.ZIP' |
|
20 |
|
|
21 |
|
|
22 |
@api.route('/upload_files') |
|
23 |
@api.expect(upload_parser) |
|
24 |
class FileStorage(Resource): |
|
25 |
def post(self): |
|
26 |
args = upload_parser.parse_args() |
|
27 |
project_name = args['project_name'] |
|
28 |
class_file = request.files['class_file'] |
|
29 |
training_file = request.files['training_file'] |
|
30 |
|
|
31 |
project_path = os.path.join(os.path.dirname(os.path.realpath(__file__)) + '\\..\\..\\symbol_training\\Data\\', |
|
32 |
project_name) |
|
33 |
|
|
34 |
try: |
|
35 |
if not os.path.isdir(project_path): |
|
36 |
os.mkdir(project_path) |
|
37 |
os.mkdir(os.path.join(project_path, 'training')) |
|
38 |
os.mkdir(os.path.join(project_path, 'training', 'xml')) |
|
39 |
os.mkdir(os.path.join(project_path, 'training', 'img')) |
|
40 |
os.mkdir(os.path.join(project_path, 'test')) |
|
41 |
os.mkdir(os.path.join(project_path, 'test', 'xml')) |
|
42 |
os.mkdir(os.path.join(project_path, 'test', 'img')) |
|
43 |
except FileNotFoundError: |
|
44 |
return make_response('File Not Found', 404) |
|
45 |
|
|
46 |
if class_file and allowed_file(class_file.filename): |
|
47 |
class_file.save(class_file.filename) |
|
48 |
with zipfile.ZipFile(class_file.filename) as zip_file: |
|
49 |
zip_file.extractall(path=project_path) |
|
50 |
else: |
|
51 |
return False |
|
52 |
|
|
53 |
if training_file and allowed_file(training_file.filename): |
|
54 |
training_file.save(training_file.filename) |
|
55 |
with zipfile.ZipFile(training_file.filename) as zip_file: |
|
56 |
zip_file.extractall(path=project_path) |
|
57 |
else: |
|
58 |
return False |
|
59 |
|
|
60 |
return True |
|
61 |
|
|
62 |
|
|
63 |
@api.route('/training') |
|
64 |
@api.expect(training_parser) |
|
65 |
class TrainingModel(Resource): |
|
66 |
def post(self): |
|
67 |
args = training_parser.parse_args() |
|
68 |
project_name = args['project_name'] |
|
69 |
class_name_file = request.files['class_name_file'] |
|
70 |
|
|
71 |
project_path = os.path.join(os.path.dirname(os.path.realpath(__file__)) + '\\..\\..\\symbol_training\\Data\\', |
|
72 |
project_name) |
|
73 |
|
|
74 |
if class_name_file: |
|
75 |
classes = class_name_file.read().decode('ascii').splitlines() |
|
76 |
else: |
|
77 |
return False |
|
78 |
|
|
79 |
return jsonify({'count': 1}) |
DTI_PID/WebServer/app/api/controller/license_controller.py | ||
---|---|---|
1 |
# file name : license_controller.py |
|
2 |
|
|
3 |
from flask import Flask, request, Blueprint |
|
4 |
from flask_restx import Api, fields, marshal_with, Resource, reqparse |
|
5 |
from ..models import LicenseDTO |
|
6 |
|
|
7 |
api = LicenseDTO.api |
|
8 |
_license = LicenseDTO.license |
|
9 |
|
|
10 |
license_service = Blueprint('api', __name__, url_prefix='/api/license') |
|
11 |
|
|
12 |
|
|
13 |
@api.route('/') |
|
14 |
@api.doc(params={'authorization': 'authorization code', 'computer_name': 'your computer name'}) |
|
15 |
class License(Resource): |
|
16 |
@api.doc('generate license key') |
|
17 |
def get(self): |
|
18 |
"""generate a license key with given authorization and computer name""" |
|
19 |
|
|
20 |
parser = reqparse.RequestParser() |
|
21 |
parser.add_argument('authorization', type=str, required=True, help='authorization can not be blank!') |
|
22 |
parser.add_argument('computer_name', type=str, required=True, help='computer name can not be blank!') |
|
23 |
args = parser.parse_args() |
|
24 |
code = self.generate_license_key(pw=args['authorization'], computer_name=args['computer_name']) |
|
25 |
|
|
26 |
return code |
|
27 |
|
|
28 |
def generate_license_key(self, pw: str, computer_name: str) -> str: |
|
29 |
import base64 |
|
30 |
|
|
31 |
key = 'Image Drawing to Intelligent Drawing' |
|
32 |
|
|
33 |
if pw != 'admin': |
|
34 |
return 'Invalid Authorization' |
|
35 |
|
|
36 |
enc = [] |
|
37 |
for i in range(len(computer_name)): |
|
38 |
key_c = key[i % len(key)] |
|
39 |
enc_c = (ord(computer_name[i]) + ord(key_c)) % 256 |
|
40 |
enc.append(enc_c) |
|
41 |
|
|
42 |
new_key = base64.urlsafe_b64encode(bytes(enc)) |
|
43 |
|
|
44 |
return new_key.decode('utf-8') |
DTI_PID/WebServer/app/api/controllers/ModelController.py | ||
---|---|---|
1 |
from flask import request, Blueprint |
|
2 |
from flask_restx import Resource |
|
3 |
|
|
4 |
from ..models.ModelDTO import ModelDTO, api |
|
5 |
|
|
6 |
resource_fields = ModelDTO.resource_fields |
|
7 |
project_service = Blueprint('api', __name__, url_prefix='/models') |
|
8 |
|
|
9 |
model_parser = api.parser() |
|
10 |
model_parser.add_argument('project_no', type=str, required=True) |
|
11 |
model_parser.add_argument('name', type=str, required=True) |
|
12 |
|
|
13 |
@api.route('/list') |
|
14 |
class ModelList(Resource): |
|
15 |
@api.doc('list_of_registered_model') |
|
16 |
@api.marshal_list_with(resource_fields, envelope='data') |
|
17 |
def get(self): |
|
18 |
"""List all registered users""" |
|
19 |
model = ModelDTO() |
|
20 |
model.project_no = 'hello' |
|
21 |
model.name = 'open' |
|
22 |
models = [model.as_dict()] # this is sample data |
|
23 |
return models |
|
24 |
|
|
25 |
@api.response(201, 'Model successfully created.') |
|
26 |
@api.doc('create a new model') |
|
27 |
@api.expect(model_parser) |
|
28 |
def post(self): |
|
29 |
"""Creates a new model""" |
|
30 |
args = model_parser.parse_args() |
|
31 |
project_no = args['project_no'] |
|
32 |
name = args['name'] |
|
33 |
|
|
34 |
return ModelList.create_new_model_if_need(project_no, name) |
|
35 |
|
|
36 |
@staticmethod |
|
37 |
def exist(project_no: str, name: str) -> bool: |
|
38 |
return True |
|
39 |
|
|
40 |
@staticmethod |
|
41 |
def create_new_model_if_need(project_no: str, name: str) -> bool: |
|
42 |
return True |
|
43 |
|
|
44 |
|
|
45 |
@api.route('/model') |
|
46 |
@api.expect(model_parser) |
|
47 |
class Model(Resource): |
|
48 |
@api.doc('get a model') |
|
49 |
@api.marshal_with(resource_fields) |
|
50 |
def get(self): |
|
51 |
"""get a model given its identifier""" |
|
52 |
args = model_parser.parse_args() |
|
53 |
project_no = args['project_no'] |
|
54 |
name = args['name'] |
|
55 |
|
|
56 |
model = ModelDTO() |
|
57 |
model.project_no = project_no |
|
58 |
model.name = name |
|
59 |
|
|
60 |
return model |
DTI_PID/WebServer/app/api/controllers/ProjectController.py | ||
---|---|---|
1 |
import os |
|
2 |
from flask import request, Blueprint, make_response |
|
3 |
from flask_restx import Resource |
|
4 |
|
|
5 |
from ..models.ProjectDTO import ProjectDTO, api |
|
6 |
|
|
7 |
resource_fields = ProjectDTO.resource_fields |
|
8 |
project_service = Blueprint('api', __name__, url_prefix='/projects') |
|
9 |
|
|
10 |
|
|
11 |
@api.route('/list') |
|
12 |
class ProjectList(Resource): |
|
13 |
@api.doc('list_of_registered_project') |
|
14 |
@api.marshal_list_with(resource_fields, envelope='data') |
|
15 |
def get(self): |
|
16 |
"""List all registered users""" |
|
17 |
project = ProjectDTO() |
|
18 |
project.name = 'hello' |
|
19 |
project.status.name = 'open' |
|
20 |
users = [project.as_dict(), {'no': 'hello'}] |
|
21 |
return users |
|
22 |
|
|
23 |
@api.response(201, 'User successfully created.') |
|
24 |
@api.doc('create a new project') |
|
25 |
@api.expect(resource_fields, validate=True) |
|
26 |
def post(self): |
|
27 |
"""Creates a new User """ |
|
28 |
data = request.json |
|
29 |
return None # save_new_user(data=data) |
|
30 |
|
|
31 |
|
|
32 |
@api.route('/<no>') |
|
33 |
@api.doc(params={'no': 'The Project identifier'}) |
|
34 |
class Project(Resource): |
|
35 |
@api.doc('get a project') |
|
36 |
@api.marshal_with(resource_fields) |
|
37 |
def get(self, no): |
|
38 |
"""get a project given its identifier""" |
|
39 |
pass |
|
40 |
""" |
|
41 |
user = get_a_user(public_id) |
|
42 |
if not user: |
|
43 |
api.abort(404) |
|
44 |
else: |
|
45 |
return user |
|
46 |
""" |
|
47 |
return ProjectDTO() |
|
48 |
|
|
49 |
@staticmethod |
|
50 |
def create_folder_if_need(project_no: str) -> bool: |
|
51 |
project_path = os.path.join(os.path.dirname(os.path.realpath(__file__)) + '\\..\\..\\symbol_training\\Data\\', |
|
52 |
project_no) |
|
53 |
try: |
|
54 |
if not os.path.isdir(project_path): |
|
55 |
os.mkdir(project_path) |
|
56 |
os.mkdir(os.path.join(project_path, 'training')) |
|
57 |
os.mkdir(os.path.join(project_path, 'training', 'xml')) |
|
58 |
os.mkdir(os.path.join(project_path, 'training', 'img')) |
|
59 |
os.mkdir(os.path.join(project_path, 'test')) |
|
60 |
os.mkdir(os.path.join(project_path, 'test', 'xml')) |
|
61 |
os.mkdir(os.path.join(project_path, 'test', 'img')) |
|
62 |
|
|
63 |
return True |
|
64 |
except Exception as ex: |
|
65 |
return False |
|
66 |
|
|
67 |
return False |
DTI_PID/WebServer/app/api/controllers/TrainingController.py | ||
---|---|---|
1 |
import os |
|
2 |
import zipfile |
|
3 |
from flask import request, Blueprint, redirect, url_for, render_template, make_response, jsonify |
|
4 |
from flask_restx import Namespace, Resource, reqparse |
|
5 |
from werkzeug.datastructures import FileStorage |
|
6 |
from .ModelController import ModelList |
|
7 |
|
|
8 |
api = Namespace('training', description='file related operations') |
|
9 |
upload_parser = api.parser() |
|
10 |
upload_parser.add_argument('project_no', type=str, required=True) |
|
11 |
upload_parser.add_argument('model_name', type=str, required=True) |
|
12 |
upload_parser.add_argument('class_file', location='files', type=FileStorage, required=True) |
|
13 |
upload_parser.add_argument('training_file', location='files', type=FileStorage, required=True) |
|
14 |
|
|
15 |
training_parser = api.parser() |
|
16 |
training_parser.add_argument('project_no', type=str, required=True) |
|
17 |
training_parser.add_argument('model_name', type=str, required=True) |
|
18 |
training_parser.add_argument('class_name_file', location='files', type=FileStorage, required=True) |
|
19 |
|
|
20 |
|
|
21 |
def allowed_file(filename): |
|
22 |
return os.path.splitext(filename)[1].upper() == '.ZIP' |
|
23 |
|
|
24 |
|
|
25 |
@api.route('/upload_files') |
|
26 |
@api.expect(upload_parser) |
|
27 |
class FileStorage(Resource): |
|
28 |
def post(self): |
|
29 |
args = upload_parser.parse_args() |
|
30 |
project_no = args['project_no'] |
|
31 |
model_name = args['model_name'] |
|
32 |
class_file = request.files['class_file'] |
|
33 |
training_file = request.files['training_file'] |
|
34 |
|
|
35 |
project_path = os.path.join(os.path.dirname(os.path.realpath(__file__)) + '\\..\\..\\symbol_training\\Data\\', |
|
36 |
project_no) |
|
37 |
|
|
38 |
try: |
|
39 |
if not os.path.isdir(project_path): |
|
40 |
os.mkdir(project_path) |
|
41 |
os.mkdir(os.path.join(project_path, 'training')) |
|
42 |
os.mkdir(os.path.join(project_path, 'training', 'xml')) |
|
43 |
os.mkdir(os.path.join(project_path, 'training', 'img')) |
|
44 |
os.mkdir(os.path.join(project_path, 'test')) |
|
45 |
os.mkdir(os.path.join(project_path, 'test', 'xml')) |
|
46 |
os.mkdir(os.path.join(project_path, 'test', 'img')) |
|
47 |
except FileNotFoundError: |
|
48 |
return make_response('File Not Found', 404) |
|
49 |
|
|
50 |
if ModelList.create_new_model_if_need(project_no, model_name): |
|
51 |
if class_file and allowed_file(class_file.filename): |
|
52 |
class_file.save(class_file.filename) |
|
53 |
with zipfile.ZipFile(class_file.filename) as zip_file: |
|
54 |
zip_file.extractall(path=project_path) |
|
55 |
else: |
|
56 |
return False |
|
57 |
|
|
58 |
if training_file and allowed_file(training_file.filename): |
|
59 |
training_file.save(training_file.filename) |
|
60 |
with zipfile.ZipFile(training_file.filename) as zip_file: |
|
61 |
zip_file.extractall(path=project_path) |
|
62 |
else: |
|
63 |
return False |
|
64 |
|
|
65 |
return True |
|
66 |
|
|
67 |
return False |
|
68 |
|
|
69 |
|
|
70 |
@api.route('/training') |
|
71 |
@api.expect(training_parser) |
|
72 |
class TrainingModel(Resource): |
|
73 |
def post(self): |
|
74 |
args = training_parser.parse_args() |
|
75 |
project_no = args['project_no'] |
|
76 |
model_name = args['model_name'] |
|
77 |
class_name_file = request.files['class_name_file'] |
|
78 |
|
|
79 |
project_path = os.path.join(os.path.dirname(os.path.realpath(__file__)) + '\\..\\..\\symbol_training\\Data\\', |
|
80 |
project_no) |
|
81 |
|
|
82 |
if ModelList.exist(project_no, model_name): |
|
83 |
if class_name_file: |
|
84 |
classes = class_name_file.read().decode('utf-8').splitlines() |
|
85 |
else: |
|
86 |
return False |
|
87 |
|
|
88 |
return jsonify({'count': 1}) |
DTI_PID/WebServer/app/api/controllers/license_controller.py | ||
---|---|---|
1 |
# file name : license_controller.py |
|
2 |
|
|
3 |
from flask import Flask, request, Blueprint |
|
4 |
from flask_restx import Api, fields, marshal_with, Resource, reqparse |
|
5 |
from ..models.LicenseDTO import LicenseDTO |
|
6 |
|
|
7 |
api = LicenseDTO.api |
|
8 |
_license = LicenseDTO.license |
|
9 |
|
|
10 |
license_service = Blueprint('api', __name__, url_prefix='/api/license') |
|
11 |
|
|
12 |
|
|
13 |
@api.route('/') |
|
14 |
@api.doc(params={'authorization': 'authorization code', 'computer_name': 'your computer name'}) |
|
15 |
class License(Resource): |
|
16 |
@api.doc('generate license key') |
|
17 |
def get(self): |
|
18 |
"""generate a license key with given authorization and computer name""" |
|
19 |
|
|
20 |
parser = reqparse.RequestParser() |
|
21 |
parser.add_argument('authorization', type=str, required=True, help='authorization can not be blank!') |
|
22 |
parser.add_argument('computer_name', type=str, required=True, help='computer name can not be blank!') |
|
23 |
args = parser.parse_args() |
|
24 |
code = self.generate_license_key(pw=args['authorization'], computer_name=args['computer_name']) |
|
25 |
|
|
26 |
return code |
|
27 |
|
|
28 |
def generate_license_key(self, pw: str, computer_name: str) -> str: |
|
29 |
import base64 |
|
30 |
|
|
31 |
key = 'Image Drawing to Intelligent Drawing' |
|
32 |
|
|
33 |
if pw != 'admin': |
|
34 |
return 'Invalid Authorization' |
|
35 |
|
|
36 |
enc = [] |
|
37 |
for i in range(len(computer_name)): |
|
38 |
key_c = key[i % len(key)] |
|
39 |
enc_c = (ord(computer_name[i]) + ord(key_c)) % 256 |
|
40 |
enc.append(enc_c) |
|
41 |
|
|
42 |
new_key = base64.urlsafe_b64encode(bytes(enc)) |
|
43 |
|
|
44 |
return new_key.decode('utf-8') |
DTI_PID/WebServer/app/api/models.py | ||
---|---|---|
1 |
# coding: utf-8 |
|
2 |
"""This is license module""" |
|
3 |
|
|
4 |
from flask_restx import Namespace, fields |
|
5 |
|
|
6 |
|
|
7 |
class LicenseDTO: |
|
8 |
api = Namespace('license', description='license related operations') |
|
9 |
license = api.model('license', { |
|
10 |
'authorization': fields.String(requried=True, description='authorization'), |
|
11 |
'computer_name': fields.String(required=True, description='computer name'), |
|
12 |
'code': fields.String(description='code') |
|
13 |
}) |
|
14 |
|
DTI_PID/WebServer/app/api/models/LicenseDTO.py | ||
---|---|---|
1 |
# coding: utf-8 |
|
2 |
"""This is license module""" |
|
3 |
|
|
4 |
from flask_restx import Namespace, fields |
|
5 |
|
|
6 |
|
|
7 |
class LicenseDTO: |
|
8 |
api = Namespace('license', description='license related operations') |
|
9 |
license = api.model('license', { |
|
10 |
'authorization': fields.String(requried=True, description='authorization'), |
|
11 |
'computer_name': fields.String(required=True, description='computer name'), |
|
12 |
'code': fields.String(description='code') |
|
13 |
}) |
|
14 |
|
DTI_PID/WebServer/app/api/models/ModelDTO.py | ||
---|---|---|
1 |
from flask_restx import Namespace, fields |
|
2 |
|
|
3 |
api = Namespace('model', description='model related operations') |
|
4 |
|
|
5 |
|
|
6 |
class ModelDTO: |
|
7 |
resource_fields = api.model('model', { |
|
8 |
'uid': fields.String(required=True, description='project unit uid'), |
|
9 |
'project_no': fields.String(required=True, description='project no'), |
|
10 |
'name': fields.String(required=True, description='model name') |
|
11 |
}) |
|
12 |
|
|
13 |
def __init__(self): |
|
14 |
self.uid = None |
|
15 |
self.project_no = None |
|
16 |
self.name = None |
|
17 |
|
|
18 |
def as_dict(self): |
|
19 |
obj_d = { |
|
20 |
'uid': self.uid, |
|
21 |
'project_no': self.project_no, |
|
22 |
'name': self.name |
|
23 |
} |
|
24 |
|
|
25 |
return obj_d |
DTI_PID/WebServer/app/api/models/ProjectDTO.py | ||
---|---|---|
1 |
from flask_restx import Namespace, fields |
|
2 |
|
|
3 |
api = Namespace('project', description='project related operations') |
|
4 |
|
|
5 |
|
|
6 |
class ProjectUnitDTO: |
|
7 |
resource_fields = api.model('project_unit', { |
|
8 |
'uid': fields.String(required=True, description='project unit uid'), |
|
9 |
'name': fields.String(required=True, description='project unit name') |
|
10 |
}) |
|
11 |
|
|
12 |
def __init__(self): |
|
13 |
self.uid = None |
|
14 |
self.name = None |
|
15 |
|
|
16 |
def as_dict(self): |
|
17 |
obj_d = { |
|
18 |
'uid': self.uid, |
|
19 |
'name': self.name, |
|
20 |
} |
|
21 |
|
|
22 |
return obj_d |
|
23 |
|
|
24 |
|
|
25 |
class ProjectStatusDTO: |
|
26 |
resource_fields = api.model('status', { |
|
27 |
'uid': fields.String(required=True, description='project uid'), |
|
28 |
'name': fields.String(required=True, description='project status name') |
|
29 |
}) |
|
30 |
|
|
31 |
def __init__(self): |
|
32 |
self.uid = None |
|
33 |
self.name = None |
|
34 |
|
|
35 |
def as_dict(self): |
|
36 |
obj_d = { |
|
37 |
'uid': self.uid, |
|
38 |
'name': self.name, |
|
39 |
} |
|
40 |
|
|
41 |
return obj_d |
|
42 |
|
|
43 |
|
|
44 |
class ProjectDTO: |
|
45 |
resource_fields = api.model('project', { |
|
46 |
'no': fields.String(required=True, description='project id', default=0), |
|
47 |
'name': fields.String(required=True, description='project name'), |
|
48 |
'unit': fields.Nested(ProjectUnitDTO.resource_fields), |
|
49 |
'description': fields.String(description='project description'), |
|
50 |
'status': fields.Nested(ProjectStatusDTO.resource_fields) |
|
51 |
}) |
|
52 |
|
|
53 |
def __init__(self): |
|
54 |
self.no = None |
|
55 |
self.name = None |
|
56 |
self.unit = ProjectUnitDTO() |
|
57 |
self.description = None |
|
58 |
self.status = ProjectStatusDTO() |
|
59 |
|
|
60 |
def as_dict(self): |
|
61 |
obj_d = { |
|
62 |
'no': self.no, |
|
63 |
'name': self.name, |
|
64 |
'unit': self.unit.as_dict(), |
|
65 |
'description': self.description, |
|
66 |
'status': self.status.as_dict() |
|
67 |
} |
|
68 |
|
|
69 |
return obj_d |
내보내기 Unified diff