Added maimai DX FESTiVAL PLUS support

- Added Card Maker support for FESTiVAL PLUS
- Bumped SDEZ database to version 8
- Updated docs for FESTiVAL PLUS
This commit is contained in:
Dniel97 2023-10-15 19:04:15 +02:00
parent 346b74e51b
commit d6e4db48f4
Signed by untrusted user: Dniel97
GPG Key ID: 6180B3C768FB2E08
14 changed files with 285 additions and 105 deletions

View File

@ -1,6 +1,13 @@
# Changelog
Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to.
## 20231015
### maimai DX
+ Added support for FESTiVAL PLUS
### Card Maker
+ Added support for maimai DX FESTiVAL PLUS
## 20230716
### General
+ Docker files added (#19)

View File

@ -0,0 +1,10 @@
ALTER TABLE mai2_profile_detail
DROP COLUMN mapStock;
ALTER TABLE mai2_profile_extend
DROP COLUMN selectResultScoreViewType;
ALTER TABLE mai2_profile_option
DROP COLUMN outFrameType,
DROP COLUMN touchVolume,
DROP COLUMN breakSlideVolume;

View File

@ -0,0 +1,10 @@
ALTER TABLE mai2_profile_detail
ADD mapStock INT NULL AFTER playCount;
ALTER TABLE mai2_profile_extend
ADD selectResultScoreViewType INT NULL AFTER selectResultDetails;
ALTER TABLE mai2_profile_option
ADD outFrameType INT NULL AFTER dispCenter,
ADD touchVolume INT NULL AFTER slideVolume,
ADD breakSlideVolume INT NULL AFTER slideVolume;

View File

@ -194,18 +194,19 @@ For versions pre-dx
| SDBM | 5 | maimai ORANGE PLUS |
| SDCQ | 6 | maimai PiNK |
| SDCQ | 7 | maimai PiNK PLUS |
| SDDK | 8 | maimai MURASAKI |
| SDDK | 9 | maimai MURASAKI PLUS |
| SDDZ | 10 | maimai MILK |
| SDDZ | 11 | maimai MILK PLUS |
| SDDK | 8 | maimai MURASAKi |
| SDDK | 9 | maimai MURASAKi PLUS |
| SDDZ | 10 | maimai MiLK |
| SDDZ | 11 | maimai MiLK PLUS |
| SDEY | 12 | maimai FiNALE |
| SDEZ | 13 | maimai DX |
| SDEZ | 14 | maimai DX PLUS |
| SDEZ | 15 | maimai DX Splash |
| SDEZ | 16 | maimai DX Splash PLUS |
| SDEZ | 17 | maimai DX Universe |
| SDEZ | 18 | maimai DX Universe PLUS |
| SDEZ | 19 | maimai DX Festival |
| SDEZ | 17 | maimai DX UNiVERSE |
| SDEZ | 18 | maimai DX UNiVERSE PLUS |
| SDEZ | 19 | maimai DX FESTiVAL |
| SDEZ | 20 | maimai DX FESTiVAL PLUS |
### Importer
@ -347,15 +348,21 @@ python dbutils.py --game SDDT upgrade
### Support status
* Card Maker 1.30:
* CHUNITHM NEW!!: Yes
* maimai DX UNiVERSE: Yes
* O.N.G.E.K.I. bright: Yes
#### Card Maker 1.30:
* CHUNITHM NEW!!: Yes
* maimai DX UNiVERSE: Yes
* O.N.G.E.K.I. bright: Yes
* Card Maker 1.35:
* CHUNITHM SUN: Yes (NEW PLUS!! up to A032)
* maimai DX FESTiVAL: Yes (up to A035) (UNiVERSE PLUS up to A031)
* O.N.G.E.K.I. bright MEMORY: Yes
#### Card Maker 1.35:
* CHUNITHM:
* NEW!!: Yes
* NEW PLUS!!: Yes (added in A028)
* SUN: Yes (added in A032)
* maimai DX:
* UNiVERSE PLUS: Yes
* FESTiVAL: Yes (added in A031)
* FESTiVAL PLUS: Yes (added in A035)
* O.N.G.E.K.I. bright MEMORY: Yes
### Importer

View File

@ -11,7 +11,7 @@ Games listed below have been tested and confirmed working. Only game versions ol
+ All versions + omnimix
+ maimai DX
+ All versions up to FESTiVAL
+ All versions up to FESTiVAL PLUS
+ Hatsune Miku: Project DIVA Arcade
+ All versions

View File

@ -205,6 +205,7 @@ class CardMakerReader(BaseReader):
"1.20": Mai2Constants.VER_MAIMAI_DX_UNIVERSE,
"1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS,
"1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL,
"1.35": Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS,
}
for root, dirs, files in os.walk(base_dir):

View File

@ -16,4 +16,4 @@ game_codes = [
Mai2Constants.GAME_CODE_GREEN,
Mai2Constants.GAME_CODE,
]
current_schema_version = 7
current_schema_version = 8

View File

@ -52,6 +52,7 @@ class Mai2Constants:
VER_MAIMAI_DX_UNIVERSE = 17
VER_MAIMAI_DX_UNIVERSE_PLUS = 18
VER_MAIMAI_DX_FESTIVAL = 19
VER_MAIMAI_DX_FESTIVAL_PLUS = 20
VERSION_STRING = (
"maimai",
@ -74,6 +75,7 @@ class Mai2Constants:
"maimai DX UNiVERSE",
"maimai DX UNiVERSE PLUS",
"maimai DX FESTiVAL",
"maimai DX FESTiVAL PLUS",
)
@classmethod

View File

@ -542,8 +542,43 @@ class Mai2DX(Mai2Base):
}
def handle_get_user_region_api_request(self, data: Dict) -> Dict:
"""
class UserRegionList:
regionId: int
playCount: int
created: str
"""
return {"userId": data["userId"], "length": 0, "userRegionList": []}
def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict:
user_id = data["userId"]
rival_id = data["rivalId"]
"""
class UserRivalData:
rivalId: int
rivalName: str
"""
return {"userId": user_id, "userRivalData": {}}
def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict:
user_id = data["userId"]
rival_id = data["rivalId"]
next_idx = data["nextIndex"]
rival_music_levels = data["userRivalMusicLevelList"]
"""
class UserRivalMusicList:
class UserRivalMusicDetailList:
level: int
achievement: int
deluxscoreMax: int
musicId: int
userRivalMusicDetailList: list[UserRivalMusicDetailList]
"""
return {"userId": user_id, "nextIndex": 0, "userRivalMusicList": []}
def handle_get_user_music_api_request(self, data: Dict) -> Dict:
user_id = data.get("userId", 0)
next_index = data.get("nextIndex", 0)

View File

@ -14,7 +14,7 @@ class Mai2Festival(Mai2UniversePlus):
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
user_data = super().handle_cm_get_user_preview_api_request(data)
# hardcode lastDataVersion for CardMaker 1.35
# hardcode lastDataVersion for CardMaker
user_data["lastDataVersion"] = "1.30.00"
return user_data
@ -25,7 +25,13 @@ class Mai2Festival(Mai2UniversePlus):
return user_login
def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict:
"""
userRecommendRateMusicIdList: list[int]
"""
return {"userId": data["userId"], "userRecommendRateMusicIdList": []}
def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict:
"""
userRecommendSelectionMusicIdList: list[int]
"""
return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []}

View File

@ -0,0 +1,38 @@
from typing import Dict
from core.config import CoreConfig
from titles.mai2.festival import Mai2Festival
from titles.mai2.const import Mai2Constants
from titles.mai2.config import Mai2Config
class Mai2FestivalPlus(Mai2Festival):
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
super().__init__(cfg, game_cfg)
self.version = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
user_data = super().handle_cm_get_user_preview_api_request(data)
# hardcode lastDataVersion for CardMaker
user_data["lastDataVersion"] = "1.35.00"
return user_data
def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict:
user_id = data.get("userId", 0)
kind = data.get("kind", 2)
next_index = data.get("nextIndex", 0)
max_ct = data.get("maxCount", 100)
is_all = data.get("isAllFavoriteItem", False)
"""
class userFavoriteItemList:
orderId: int
id: int
"""
return {
"userId": user_id,
"kind": kind,
"nextIndex": 0,
"userFavoriteItemList": [],
}

View File

@ -23,6 +23,7 @@ from titles.mai2.splashplus import Mai2SplashPlus
from titles.mai2.universe import Mai2Universe
from titles.mai2.universeplus import Mai2UniversePlus
from titles.mai2.festival import Mai2Festival
from titles.mai2.festivalplus import Mai2FestivalPlus
class Mai2Servlet:
@ -55,6 +56,7 @@ class Mai2Servlet:
Mai2Universe,
Mai2UniversePlus,
Mai2Festival,
Mai2FestivalPlus,
]
self.logger = logging.getLogger("mai2")
@ -110,17 +112,29 @@ class Mai2Servlet:
)
def setup(self):
if self.game_cfg.uploads.photos and self.game_cfg.uploads.photos_dir and not path.exists(self.game_cfg.uploads.photos_dir):
if (
self.game_cfg.uploads.photos
and self.game_cfg.uploads.photos_dir
and not path.exists(self.game_cfg.uploads.photos_dir)
):
try:
mkdir(self.game_cfg.uploads.photos_dir)
except Exception:
self.logger.error(f"Failed to make photo upload directory at {self.game_cfg.uploads.photos_dir}")
self.logger.error(
f"Failed to make photo upload directory at {self.game_cfg.uploads.photos_dir}"
)
if self.game_cfg.uploads.movies and self.game_cfg.uploads.movies_dir and not path.exists(self.game_cfg.uploads.movies_dir):
if (
self.game_cfg.uploads.movies
and self.game_cfg.uploads.movies_dir
and not path.exists(self.game_cfg.uploads.movies_dir)
):
try:
mkdir(self.game_cfg.uploads.movies_dir)
except Exception:
self.logger.error(f"Failed to make movie upload directory at {self.game_cfg.uploads.movies_dir}")
self.logger.error(
f"Failed to make movie upload directory at {self.game_cfg.uploads.movies_dir}"
)
def render_POST(self, request: Request, version: int, url_path: str) -> bytes:
if url_path.lower() == "ping":
@ -140,18 +154,20 @@ class Mai2Servlet:
if request.uri.startswith(b"/SDEZ"):
if version < 105: # 1.0
internal_ver = Mai2Constants.VER_MAIMAI_DX
elif version >= 105 and version < 110: # Plus
elif version >= 105 and version < 110: # PLUS
internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS
elif version >= 110 and version < 115: # Splash
internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH
elif version >= 115 and version < 120: # Splash Plus
elif version >= 115 and version < 120: # Splash PLUS
internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS
elif version >= 120 and version < 125: # Universe
elif version >= 120 and version < 125: # UNiVERSE
internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE
elif version >= 125 and version < 130: # Universe Plus
elif version >= 125 and version < 130: # UNiVERSE PLUS
internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS
elif version >= 130: # Festival
elif version >= 130 and version < 135: # FESTiVAL
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL
elif version >= 135: # FESTiVAL PLUS
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS
else:
if version < 110: # 1.0
@ -181,14 +197,19 @@ class Mai2Servlet:
elif version >= 197: # Finale
internal_ver = Mai2Constants.VER_MAIMAI_FINALE
if request.getHeader('Mai-Encoding') is not None or request.getHeader('X-Mai-Encoding') is not None:
if (
request.getHeader("Mai-Encoding") is not None
or request.getHeader("X-Mai-Encoding") is not None
):
# The has is some flavor of MD5 of the endpoint with a constant bolted onto the end of it.
# See cake.dll's Obfuscator function for details. Hopefully most DLL edits will remove
# these two(?) headers to not cause issues, but given the general quality of SEGA data...
enc_ver = request.getHeader('Mai-Encoding')
enc_ver = request.getHeader("Mai-Encoding")
if enc_ver is None:
enc_ver = request.getHeader('X-Mai-Encoding')
self.logger.debug(f"Encryption v{enc_ver} - User-Agent: {request.getHeader('User-Agent')}")
enc_ver = request.getHeader("X-Mai-Encoding")
self.logger.debug(
f"Encryption v{enc_ver} - User-Agent: {request.getHeader('User-Agent')}"
)
try:
unzip = zlib.decompress(req_raw)
@ -231,9 +252,11 @@ class Mai2Servlet:
self.logger.debug(f"v{version} GET {url_path}")
url_split = url_path.split("/")
if (url_split[0] == "api" and url_split[1] == "movie") or url_split[0] == "movie":
if (url_split[0] == "api" and url_split[1] == "movie") or url_split[
0
] == "movie":
if url_split[2] == "moviestart":
return json.dumps({"moviestart":{"status":"OK"}}).encode()
return json.dumps({"moviestart": {"status": "OK"}}).encode()
else:
request.setResponseCode(404)
@ -261,8 +284,12 @@ class Mai2Servlet:
self.logger.info(f"v{version} usbdl server test")
return b""
elif self.game_cfg.deliver.udbdl_enable and path.exists(f"{self.game_cfg.deliver.content_folder}/usb/{url_split[-1]}"):
with open(f"{self.game_cfg.deliver.content_folder}/usb/{url_split[-1]}", 'rb') as f:
elif self.game_cfg.deliver.udbdl_enable and path.exists(
f"{self.game_cfg.deliver.content_folder}/usb/{url_split[-1]}"
):
with open(
f"{self.game_cfg.deliver.content_folder}/usb/{url_split[-1]}", "rb"
) as f:
return f.read()
else:
@ -272,10 +299,16 @@ class Mai2Servlet:
elif url_split[0] == "deliver":
file = url_split[len(url_split) - 1]
self.logger.info(f"v{version} {file} deliver inquire")
self.logger.debug(f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}")
self.logger.debug(
f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}"
)
if self.game_cfg.deliver.enable and path.exists(f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}"):
with open(f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}", 'rb') as f:
if self.game_cfg.deliver.enable and path.exists(
f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}"
):
with open(
f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}", "rb"
) as f:
return f.read()
else:

View File

@ -40,6 +40,7 @@ detail = Table(
Column("charaLockSlot", JSON),
Column("contentBit", BigInteger),
Column("playCount", Integer),
Column("mapStock", Integer), # new with fes+
Column("eventWatchedDate", String(25)),
Column("lastGameId", String(25)),
Column("lastRomVersion", String(25)),
@ -100,7 +101,7 @@ detail = Table(
)
detail_old = Table(
"maimai_profile_detail",
"maimai_profile_detail",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
@ -216,6 +217,7 @@ extend = Table(
Column("isPhotoAgree", Boolean),
Column("isGotoCodeRead", Boolean),
Column("selectResultDetails", Boolean),
Column("selectResultScoreViewType", Integer), # new with fes+
Column("sortCategorySetting", Integer),
Column("sortMusicSetting", Integer),
Column("selectedCardList", JSON),
@ -251,6 +253,7 @@ option = Table(
Column("touchSize", Integer),
Column("starRotate", Integer),
Column("dispCenter", Integer),
Column("outFrameType", Integer), # new with fes+
Column("dispChain", Integer),
Column("dispRate", Integer),
Column("dispBar", Integer),
@ -276,6 +279,8 @@ option = Table(
Column("exVolume", Integer),
Column("slideSe", Integer),
Column("slideVolume", Integer),
Column("breakSlideVolume", Integer), # new with fes+
Column("touchVolume", Integer), # new with fes+
Column("touchHoldVolume", Integer),
Column("damageSeVolume", Integer),
Column("headPhoneVolume", Integer),
@ -438,7 +443,11 @@ boss = Table(
"maimai_profile_boss",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("pandoraFlagList0", Integer),
Column("pandoraFlagList1", Integer),
Column("pandoraFlagList2", Integer),
@ -455,23 +464,32 @@ recent_rating = Table(
"maimai_profile_recent_rating",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("userRecentRatingList", JSON),
UniqueConstraint("user", name="mai2_profile_recent_rating_uk"),
mysql_charset="utf8mb4",
)
consec_logins = Table(
"mai2_profile_consec_logins",
"mai2_profile_consec_logins",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("logins", Integer),
UniqueConstraint("user", "version", name="mai2_profile_consec_logins_uk"),
mysql_charset="utf8mb4",
)
class Mai2ProfileData(BaseData):
def put_profile_detail(
self, user_id: int, version: int, detail_data: Dict, is_dx: bool = True
@ -494,7 +512,9 @@ class Mai2ProfileData(BaseData):
return None
return result.lastrowid
def get_profile_detail(self, user_id: int, version: int, is_dx: bool = True) -> Optional[Row]:
def get_profile_detail(
self, user_id: int, version: int, is_dx: bool = True
) -> Optional[Row]:
if is_dx:
sql = (
select(detail)
@ -505,7 +525,9 @@ class Mai2ProfileData(BaseData):
else:
sql = (
select(detail_old)
.where(and_(detail_old.c.user == user_id, detail_old.c.version <= version))
.where(
and_(detail_old.c.user == user_id, detail_old.c.version <= version)
)
.order_by(detail_old.c.version.desc())
)
@ -582,11 +604,15 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warning(f"put_profile_option: failed to update! {user_id} is_dx {is_dx}")
self.logger.warning(
f"put_profile_option: failed to update! {user_id} is_dx {is_dx}"
)
return None
return result.lastrowid
def get_profile_option(self, user_id: int, version: int, is_dx: bool = True) -> Optional[Row]:
def get_profile_option(
self, user_id: int, version: int, is_dx: bool = True
) -> Optional[Row]:
if is_dx:
sql = (
select(option)
@ -596,7 +622,9 @@ class Mai2ProfileData(BaseData):
else:
sql = (
select(option_old)
.where(and_(option_old.c.user == user_id, option_old.c.version <= version))
.where(
and_(option_old.c.user == user_id, option_old.c.version <= version)
)
.order_by(option_old.c.version.desc())
)
@ -689,7 +717,9 @@ class Mai2ProfileData(BaseData):
return None
return result.fetchall()
def put_web_option(self, user_id: int, version: int, web_opts: Dict) -> Optional[int]:
def put_web_option(
self, user_id: int, version: int, web_opts: Dict
) -> Optional[int]:
web_opts["user"] = user_id
web_opts["version"] = version
sql = insert(web_opt).values(**web_opts)
@ -698,14 +728,14 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warning(
f"put_web_option: failed to update! user_id: {user_id}"
)
self.logger.warning(f"put_web_option: failed to update! user_id: {user_id}")
return None
return result.lastrowid
def get_web_option(self, user_id: int, version: int) -> Optional[Row]:
sql = web_opt.select(and_(web_opt.c.user == user_id, web_opt.c.version == version))
sql = web_opt.select(
and_(web_opt.c.user == user_id, web_opt.c.version == version)
)
result = self.execute(sql)
if result is None:
@ -742,9 +772,7 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warning(
f"put_boss_list: failed to update! user_id: {user_id}"
)
self.logger.warning(f"put_boss_list: failed to update! user_id: {user_id}")
return None
return result.lastrowid
@ -759,7 +787,7 @@ class Mai2ProfileData(BaseData):
def put_recent_rating(self, user_id: int, rr: Dict) -> Optional[int]:
sql = insert(recent_rating).values(user=user_id, userRecentRatingList=rr)
conflict = sql.on_duplicate_key_update({'userRecentRatingList': rr})
conflict = sql.on_duplicate_key_update({"userRecentRatingList": rr})
result = self.execute(conflict)
if result is None:
@ -778,25 +806,23 @@ class Mai2ProfileData(BaseData):
return result.fetchone()
def add_consec_login(self, user_id: int, version: int) -> None:
sql = insert(consec_logins).values(
user=user_id,
version=version,
logins=1
)
sql = insert(consec_logins).values(user=user_id, version=version, logins=1)
conflict = sql.on_duplicate_key_update(
logins=consec_logins.c.logins + 1
)
conflict = sql.on_duplicate_key_update(logins=consec_logins.c.logins + 1)
result = self.execute(conflict)
if result is None:
self.logger.error(f"Failed to update consecutive login count for user {user_id} version {version}")
self.logger.error(
f"Failed to update consecutive login count for user {user_id} version {version}"
)
def get_consec_login(self, user_id: int, version: int) -> Optional[Row]:
sql = select(consec_logins).where(and_(
consec_logins.c.user==user_id,
consec_logins.c.version==version,
))
sql = select(consec_logins).where(
and_(
consec_logins.c.user == user_id,
consec_logins.c.version == version,
)
)
result = self.execute(sql)
if result is None:
@ -804,12 +830,12 @@ class Mai2ProfileData(BaseData):
return result.fetchone()
def reset_consec_login(self, user_id: int, version: int) -> Optional[Row]:
sql = consec_logins.update(and_(
consec_logins.c.user==user_id,
consec_logins.c.version==version,
)).values(
logins=1
)
sql = consec_logins.update(
and_(
consec_logins.c.user == user_id,
consec_logins.c.version == version,
)
).values(logins=1)
result = self.execute(sql)
if result is None:

View File

@ -23,10 +23,11 @@ class Mai2Universe(Mai2SplashPlus):
return {
"userName": p["userName"],
"rating": p["playerRating"],
# hardcode lastDataVersion for CardMaker 1.34
# hardcode lastDataVersion for CardMaker
"lastDataVersion": "1.20.00",
# checks if the user is still logged in
"isLogin": False,
"isExistSellingCard": False,
"isExistSellingCard": True,
}
def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
@ -70,8 +71,12 @@ class Mai2Universe(Mai2SplashPlus):
tmp.pop("cardName")
tmp.pop("enabled")
tmp["startDate"] = datetime.strftime(tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT)
tmp["endDate"] = datetime.strftime(tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT)
tmp["startDate"] = datetime.strftime(
tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT
)
tmp["endDate"] = datetime.strftime(
tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT
)
tmp["noticeStartDate"] = datetime.strftime(
tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT
)