def doc(self, collection: str, oid: Union[ObjectId, str], query: Union[list, None], reference_delete=True) -> dict: """ | refもしくはembのドキュメントを取得する | オプションでedman特有のデータ含んで取得することもできる :param str collection: :param oid: :type oid: ObjectId or str :param query: :type query: list or None :param bool reference_delete: default True :return: result :rtype: dict """ oid = Utils.conv_objectid(oid) doc = self.db[collection].find_one({'_id': oid}) if doc is None: sys.exit('ドキュメントが存在しません') # embの場合は指定階層のドキュメントを引き抜く # refの場合はdocの結果をそのまま入れる doc_result = self._get_emb_doc(doc, query) if query is not None else doc # クエリの指定によってはリストデータなども取得出てしまうため if not isinstance(doc_result, dict): sys.exit(f'指定されたクエリはドキュメントではありません {query}') result = Utils.reference_item_delete( doc_result, ('_id', self.parent, self.child, self.file_ref) ) if reference_delete else doc_result return result
def test_collection_name_check(self): illegals = [None, '', '$aaa', 'aaa$b', 'system.aaa', '#aaa', '@aaa'] for i in illegals: with self.subTest(i=i): actual = Utils.collection_name_check(i) self.assertFalse(actual) # 文字列以外の方は文字列に変換される actual = Utils.collection_name_check(345) self.assertTrue(actual)
def test__to_datetime(self): # datetime正常 input_list = ['2018/11/20', '2018/11/20 13:48', '2018/01/01 00:00:00'] for s in input_list: with self.subTest(s=s): actual = Utils.to_datetime(s) self.assertIsInstance(actual, datetime) # 入力値が文字列だがdatetime変換できない場合、または入力値が文字列以外 input_list = [20181120, 201811201348, 20200101000000, '8月12日', None] for s in input_list: with self.subTest(s=s): actual = Utils.to_datetime(s) self.assertIsInstance(actual, str)
def _convert_datetime(self, child_dict: dict) -> dict: """ | 辞書内辞書になっている文字列日付時間データを、辞書内日付時間に変換 | (例) | {'start_date': {'#date': '1981-04-23'}} | から | {'start_date': 1981-04-23T00:00:00} :param dict child_dict: :return: result :rtype: dict """ result = copy.deepcopy(child_dict) if isinstance(child_dict, dict): try: for key, value in child_dict.items(): if isinstance(value, dict) and self.date in value: result.update({ key: Utils.to_datetime(child_dict[key][self.date]) }) except AttributeError: sys.exit(f'日付変換に失敗しました.構造に問題があります. {child_dict}') return result
def get_file_ref(self, doc: dict, structure: str, query=None) -> list: """ ファイルリファレンス情報を取得 :param dict doc: :param str structure: :param query: :type: list or None :return: files_list :rtype: list """ if structure == 'emb' and query is None: sys.exit('embにはクエリが必要です') if structure != 'emb' and structure != 'ref': sys.exit('構造の選択はembまたはrefが必要です') files_list = [] if structure == 'ref': if self.file_ref in doc: files_list = doc[self.file_ref] else: if not Utils.query_check(query, doc): sys.exit('対象のドキュメントに対してクエリーが一致しません.') # docから対象クエリを利用してファイルのリストを取得 # deepcopyを使用しないとなぜか子のスコープのqueryがクリヤーされる query_c = copy.deepcopy(query) try: files_list = self._get_emb_files_list(doc, query_c) except Exception as e: sys.exit(e) return files_list
def child_delete(doc_with_child: dict) -> None: """ 子要素を削除する :param dict doc_with_child: :return: """ tmp = copy.deepcopy(doc_with_child) tmp_list = [] # 子要素のデータを抽出 for k, v in doc_with_child.items(): if self.parent != k and self.child != k and (isinstance( v, dict) or (isinstance(v, list) and (not Utils.item_literal_check(v)))): tmp_list.append(k) # 該当データがtmpにあれば削除 for j in tmp_list: if j in tmp: del tmp[j] # outputのデータを入れ替える if collection in output: output[collection].append(tmp) else: output[collection] = [tmp]
def _collect_emb_file_ref(self, doc: dict, request_key: str) -> list: """ emb構造のデータからファイルリファレンスのリストだけを取り出すジェネレータ :param dict doc: :param str request_key: :return: value :rtype: list """ for key, value in doc.items(): if isinstance(value, dict): yield from self._collect_emb_file_ref(value, request_key) elif isinstance(value, list) and Utils.item_literal_check(value): if key == request_key: yield value continue elif isinstance(value, list): if key == request_key: yield value else: for i in value: yield from self._collect_emb_file_ref(i, request_key) else: continue
def _get_child_reference(self, child_data: dict) -> dict: """ 子データのリファレンス情報を作成して取得 :param dict child_data: :return: :rtype: dict """ children = [] for collection, child_value in child_data.items(): # すでにparentが作られている場合は飛ばす if self.parent == collection: continue if isinstance(child_value, dict): children.append(DBRef(collection, child_value['_id'])) elif isinstance( child_value, list) and (not Utils.item_literal_check(child_value)): child_list = [DBRef(collection, j['_id']) for j in child_value] children.extend(child_list) else: continue return {self.child: children}
def get_file_names(self, collection: str, oid: Union[ObjectId, str], structure: str, query: Union[list, None]) -> dict: """ ファイル一覧を取得 :param str collection: :param str oid: :param str structure: :param query: embの時だけ必要. refの時はNone :type query: list or None :return: result :rtype: dict """ oid = Utils.conv_objectid(oid) # ドキュメント存在確認&コレクション存在確認&対象ドキュメント取得 doc = self.db[collection].find_one({'_id': oid}) if doc is None: sys.exit('対象のコレクション、またはドキュメントが存在しません') # ファイルリスト取得 files_list = self.get_file_ref(doc, structure, query) result = {} if files_list: # ファイルリストを元にgridfsからファイル名を取り出す for file_oid in files_list: fs_out = self.fs.get(file_oid) result.update({file_oid: fs_out.filename}) else: sys.exit('関連ファイルはありません') return result
def test__query_check(self): # 正常系 query = ['bbb', '2', 'eee', '0', 'fff'] doc = { 'aaa': '123', 'bbb': [{ 'ccc': '456' }, { 'ddd': '789' }, { 'eee': [{ 'fff': { 'ans': 'OK' } }, { 'ggg': '1' }] }] } actual = Utils.query_check(query, doc) self.assertIsInstance(actual, bool) self.assertTrue(actual) # 異常系 間違ったクエリ query = ['bbb', '2', 'eee', '1', 'fff'] # インデックスの指定ミスを想定 doc = { 'aaa': '123', 'bbb': [{ 'ccc': '456' }, { 'ddd': '789' }, { 'eee': [{ 'fff': { 'ans': 'OK' } }, { 'ggg': '1' }] }] } actual = Utils.query_check(query, doc) self.assertIsInstance(actual, bool) self.assertFalse(actual)
def recursive(data: dict): # idとrefの削除 for key, val in data.items(): if isinstance(data[key], dict): recursive(Utils.reference_item_delete(data[key], refs)) # リストデータは中身を型変換する elif isinstance(data[key], list) and Utils.item_literal_check( data[key]): data[key] = [self._format_datetime(i) for i in data[key]] elif isinstance(data[key], list): for item in data[key]: recursive(Utils.reference_item_delete(item, refs)) else: try: # 型変換 data[key] = self._format_datetime(data[key]) except Exception as e: sys.exit(e)
def recursive(data: dict) -> dict: """ 再帰で辞書を走査して、日付データの変換などを行う 要リファクタリング :param dict data: :return: :rtype: dict """ output = {} for key, value in data.items(): if isinstance(value, dict): if not Utils.collection_name_check(key): sys.exit(f'この名前は使用できません {key}') converted_value = self._convert_datetime(value) output.update({key: recursive(converted_value)}) elif isinstance(value, list): # 日付データが含まれていたらdatetimeオブジェクトに変換 value = self._date_replace(value) # 通常のリストデータの場合 if Utils.item_literal_check(value): if not self._field_name_check(key): sys.exit(f'フィールド名に不備があります {key}') list_tmp_data = value # 子要素としてのリストデータの場合 else: if not Utils.collection_name_check(key): sys.exit(f'この名前は使用できません {key}') list_tmp_data = [ recursive(self._convert_datetime(i)) for i in value ] output.update({key: list_tmp_data}) else: if not self._field_name_check(key): sys.exit(f'フィールド名に不備があります {key}') output.update({key: value}) return output
def test_conv_objectid(self): # 正常系 oidの場合 oid = ObjectId() actual = Utils.conv_objectid(oid) self.assertIsInstance(actual, ObjectId) self.assertEqual(oid, actual) # 正常系 文字列の場合 oid = ObjectId() actual = Utils.conv_objectid(str(oid)) self.assertIsInstance(actual, ObjectId) self.assertEqual(oid, actual) # 異常系 oidにならない文字列 oid = str(ObjectId()) oid = oid[:-1] with self.assertRaises((SystemExit, errors.InvalidId)) as cm: _ = Utils.conv_objectid(oid)
def test__item_literal_check(self): # 正常系 リスト内が全てリテラルデータ data = [1, 2, 3] self.assertTrue(Utils.item_literal_check(data)) # 正常系 リスト内にオブジェクトを含むリテラルデータ data = [1, 2, ObjectId()] self.assertTrue(Utils.item_literal_check(data)) # 正常系 リスト内に辞書 data = [1, 2, {'d': 'bb'}] self.assertFalse(Utils.item_literal_check(data)) # 正常系 リスト内にリスト data = [1, 2, ['1', 2]] self.assertFalse(Utils.item_literal_check(data)) # 正常系 入力が辞書 data = {'d': '34'} self.assertFalse(Utils.item_literal_check(data))
def test__reference_item_delete(self): # 正常系 doc = { self.parent: ObjectId(), self.child: [ObjectId(), ObjectId()], self.file: [ObjectId(), ObjectId()], 'param': 'OK' } actual = Utils.reference_item_delete( doc, ('_id', self.parent, self.child, self.file)) expected = {'param': 'OK'} self.assertDictEqual(actual, expected)
def item_delete(self, collection: str, oid: Union[ObjectId, str], delete_key: str, query: Union[list, None]) -> bool: """ ドキュメントの項目を削除する :param str collection: :param oid: :type oid: str or ObjectId :param str delete_key: :param query: :type query: list or None :return: :rtype: bool """ oid = Utils.conv_objectid(oid) doc = self.db[collection].find_one({'_id': oid}) if doc is None: sys.exit('ドキュメントが存在しません') if query is not None: # emb try: doc = Utils.doc_traverse(doc, [delete_key], query, self._delete_execute) except Exception as e: sys.exit(e) else: # ref try: del doc[delete_key] except IndexError: sys.exit(f'キーは存在しません: {delete_key}') # ドキュメント置き換え処理 replace_result = self.db[collection].replace_one({'_id': oid}, doc) result = True if replace_result.modified_count == 1 else False return result
def delete(self, oid: Union[str, ObjectId], collection: str, structure: str) -> bool: """ | ドキュメントを削除する | 指定のoidを含む下位のドキュメントを全削除 | refで親が存在する時は親のchildリストから指定のoidを取り除く :param oid: :type oid: str or ObjectId :param str collection: :param str structure: :return: :rtype: bool """ oid = Utils.conv_objectid(oid) db_result = self.db[collection].find_one({'_id': oid}) if db_result is None: sys.exit('該当するドキュメントは存在しません') if structure == 'emb': try: result = self.db[collection].delete_one({'_id': oid}) if result.deleted_count: # 添付データがあればgridfsから削除 file = File(self.get_db) file.fs_delete( sum([i for i in self._collect_emb_file_ref( db_result, self.file_ref)], [])) return True else: sys.exit('指定のドキュメントは削除できませんでした' + str(oid)) except ValueError as e: sys.exit(e) elif structure == 'ref': try: # 親ドキュメントがあれば子要素リストから削除する if db_result.get(self.parent): self._delete_reference_from_parent(db_result[self.parent], db_result['_id']) # 対象のドキュメント以下のドキュメントと関連ファイルを削除する self._delete_documents_and_files(db_result, collection) return True except ValueError as e: sys.exit(e) else: sys.exit('structureはrefまたはembの指定が必要です')
def _convert_datetime_dict(self, amend: dict) -> dict: """ 辞書内辞書になっている文字列日付時間データを、辞書内日付時間に変換 (例) {'start_date': {'#date': '1981-04-23'}} から {'start_date': 1981-04-23T00:00:00} amendにリストデータがある場合は中身も変換対象とする :param dict amend: :return: result :rtype: dict """ result = copy.deepcopy(amend) if isinstance(amend, dict): try: for key, value in amend.items(): if isinstance(value, dict) and self.date in value: result.update( { key: Utils.to_datetime( amend[key][self.date]) }) elif isinstance(value, list): buff = [Utils.to_datetime(i[self.date]) if isinstance(i, dict) and self.date in i else i for i in value] result.update({key: buff}) else: result.update({key: value}) except AttributeError: sys.exit(f'日付変換に失敗しました.構造に問題があります. {amend}') return result
def _date_replace(self, list_data: list) -> list: """ | リスト内の要素に{'#date':日付時間}のデータが含まれていたら | datetimeオブジェクトに変換する | 例 | [{'#date':2019-02-28 11:43:22}, ' test_date'] | ↓ | [datetime.datetime(2019, 2, 28, 11, 43, 22), 'test_date'] :param list list_data: :return: :rtype: list """ return [ Utils.to_datetime(i[self.date]) if isinstance(i, dict) and self.date in i else i for i in list_data ]
def get_structure(self, collection: str, oid: ObjectId) -> str: """ 対象のドキュメントの構造を取得する :param str collection: :param ObjectId oid: :return: ref or emb :rtype: str """ doc = self.db[collection].find_one({'_id': Utils.conv_objectid(oid)}) if doc is None: sys.exit('指定のドキュメントがありません') if any(key in doc for key in (self.parent, self.child)): return 'ref' else: return 'emb'
def recursive(doc): output = {} for key, value in doc.items(): if isinstance(value, dict): if key in ex_keys: continue output.update({key: recursive(value)}) elif isinstance(value, list) and Utils.item_literal_check(value): output.update({key: value}) elif isinstance(value, list): if key in ex_keys: continue output.update({key: [recursive(i) for i in value]}) else: output.update({key: value}) return output
def recursive(data): for key, value in data.items(): if isinstance(value, dict): for del_key in reference: if del_key in value: del value[del_key] recursive(value) elif isinstance(value, list) and Utils.item_literal_check( value): continue elif isinstance(value, list): for i in value: for del_key in reference: if del_key in i: del i[del_key] recursive(i) else: pass
def update(self, collection: str, oid: Union[str, ObjectId], amend_data: dict, structure: str) -> bool: """ 修正データを用いてDBデータをアップデート :param str collection: :param oid: :type oid: str or ObjectId :param dict amend_data: :param str structure: :return: :rtype: bool """ oid = Utils.conv_objectid(oid) db_result = self.db[collection].find_one({'_id': oid}) if db_result is None: sys.exit('該当するドキュメントは存在しません') if structure == 'emb': try: # 日付データを日付オブジェクトに変換するため、 # 必ずコンバートしてからマージする convert = Convert() converted_amend_data = convert.emb(amend_data) amended = self._merge(db_result, converted_amend_data) except ValueError as e: sys.exit(e) elif structure == 'ref': # 日付データを日付オブジェクトに変換 converted_amend_data = self._convert_datetime_dict(amend_data) amended = {**db_result, **converted_amend_data} else: sys.exit('structureはrefまたはembの指定が必要です') try: replace_result = self.db[collection].replace_one({'_id': oid}, amended) except errors.OperationFailure: sys.exit('アップデートに失敗しました') return True if replace_result.modified_count == 1 else False
def recursive(doc): for key, value in doc.items(): # 最初に発見した要素がoutputに入っていたら再帰を終了 if output: break if isinstance(value, dict): if key == pull_key: output.update({key: value}) recursive(value) elif isinstance(value, list) and Utils.item_literal_check(value): pass elif isinstance(value, list): if key == pull_key: output.update({key: value}) tmp_list = [] for i in value: tmp_list.append(recursive(i)) else: pass
def _merge(self, orig: dict, amend: dict) -> dict: """ 辞書(オリジナル)と修正データをマージする :param dict orig: :param dict amend: :return: result :rtype: dict """ result = copy.copy(orig) for item in amend: if isinstance(amend[item], dict): result[item] = self._merge(orig[item], amend[item]) elif isinstance(amend[item], list): if Utils.item_literal_check(amend[item]): result[item] = amend[item] else: result[item] = self._merge_list(orig[item], amend[item]) else: result[item] = amend[item] return result
def find_collection_from_objectid(self, oid: Union[str, ObjectId]) -> Union[str, None]: """ | DB内のコレクションから指定のObjectIDを探し、所属しているコレクションを返す | DBに負荷がかかるので使用は注意が必要 :param oid: :type oid: ObjectId or str :return: collection :rtype: str or None """ oid = Utils.conv_objectid(oid) result = None coll_filter = {"name": {"$regex": r"^(?!system\.)"}} for collection in self.db.list_collection_names(filter=coll_filter): find_oid = self.db[collection].find_one({'_id': oid}) if find_oid is not None and '_id' in find_oid: result = collection break return result
def _build_to_doc_child(self, find_result: list) -> dict: """ 子の検索結果(リスト)を入れ子辞書に組み立てる :param list find_result: :return: :rtype: dict """ find_result = [i for i in Utils.child_combine(find_result)] parent_id_dict = self._generate_parent_id_dict(find_result) find_result_cp = copy.deepcopy(list(reversed(find_result))) for bros_idx, bros in enumerate(reversed(find_result)): for collection, docs in bros.items(): for doc_idx, doc in enumerate(docs): # 子データが存在する場合はマージする if doc['_id'] in parent_id_dict: tmp = find_result_cp[bros_idx][collection][doc_idx] tmp.update(parent_id_dict[doc['_id']]) doc.update(tmp) del parent_id_dict[doc['_id']] return find_result[0]
def test_child_combine(self): # データ構造のテスト test_data = [[{ 'collection_A': { 'name': 'NSX' } }, { 'collection_A': { 'name': 'F355' } }, { 'collection_B': { 'power': 280 } }]] # # test_data = [ # {'collection_A': {'name': 'NSX'}}, # {'collection_A': {'name': 'F355'}}, # {'collection_B': {'power': 280}} # ] actual = [i for i in Utils.child_combine(test_data)] self.assertIsInstance(actual, list) self.assertEqual(2, len(actual[0]['collection_A']))
def structure(self, collection: str, oid: ObjectId, structure_mode: str, new_collection: str) -> list: """ 構造をrefからembへ、またはembからrefへ変更する :param str collection: :param ObjectId oid: :param str structure_mode: :param str new_collection: :return: structured_result :rtype: list """ oid = Utils.conv_objectid(oid) # refデータをembに変換する if structure_mode == 'emb': # 自分データ取り出し ref_result = self.doc(collection, oid, query=None, reference_delete=False) reference_point_result = self.get_reference_point( ref_result) if reference_point_result[self.child]: # 子データを取り出し children = self.get_child_all({collection: ref_result}) # 自分のリファレンスデータとidを削除 for del_key in (self.parent, self.child, '_id'): if del_key in ref_result: del ref_result[del_key] # 子のリファレンスデータ削除 non_ref_children = self.delete_reference(children, ('_id', self.parent, self.child)) # 自分と子要素をマージする ref_result.update(non_ref_children) convert = Convert() converted_edman = convert.dict_to_edman( {new_collection: ref_result}, mode='emb') structured_result = self.insert(converted_edman) # 子が存在しないドキュメントの場合(新たなコレクションとして切り出す) else: # 自分のリファレンスデータとidを削除 for del_key in (self.parent, '_id'): if del_key in ref_result: del ref_result[del_key] convert = Convert() converted_edman = convert.dict_to_edman( {new_collection: ref_result}, mode='emb') structured_result = self.insert(converted_edman) # embからrefに変換 elif structure_mode == 'ref': emb_result = self.db[collection].find_one({'_id': oid}) del emb_result['_id'] convert = Convert() converted_edman = convert.dict_to_edman( {new_collection: emb_result}, mode='ref') structured_result = self.insert(converted_edman) structured_result.reverse() else: sys.exit('構造はrefかembを指定してください') return structured_result
def add_file_reference(self, collection: str, oid: Union[ObjectId, str], file_path: Tuple[Path], structure: str, query=None, compress=False) -> bool: """ ドキュメントにファイルリファレンスを追加する ファイルのインサート処理、圧縮処理なども行う :param str collection: :param oid: :type oid: ObjectId or str :param tuple file_path: :param str structure: :param query: :type query: list or None :param bool compress: default False :return: :rtype: bool """ oid = Utils.conv_objectid(oid) # ドキュメント存在確認&対象ドキュメント取得 doc = self.db[collection].find_one({'_id': oid}) if doc is None: sys.exit('対象のドキュメントが存在しません') if structure == 'emb': # クエリーがドキュメントのキーとして存在するかチェック if not Utils.query_check(query, doc): sys.exit('対象のドキュメントに対してクエリーが一致しません.') # ファイルのインサート inserted_file_oids = [] for file in self.file_gen(file_path): file_obj = file[1] metadata = {'filename': file[0]} if compress: file_obj = gzip.compress(file_obj, compresslevel=6) metadata.update({'compress': 'gzip'}) inserted_file_oids.append(self.fs.put(file_obj, **metadata)) if structure == 'ref': new_doc = self._file_list_attachment(doc, inserted_file_oids) elif structure == 'emb': try: new_doc = Utils.doc_traverse(doc, inserted_file_oids, query, self._file_list_attachment) except Exception as e: sys.exit(e) else: sys.exit('構造はrefかembが必要です') # ドキュメント差し替え replace_result = self.db[collection].replace_one({'_id': oid}, new_doc) if replace_result.modified_count == 1: return True else: # 差し替えができなかった時は添付ファイルは削除 self.fs_delete(inserted_file_oids) return False