missionpanel 1.0.1__tar.gz → 1.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: missionpanel
3
- Version: 1.0.1
3
+ Version: 1.0.2
4
4
  Summary: A mission panel
5
5
  Home-page: https://github.com/yindaheng98/missionpanel
6
6
  Author: yindaheng98
@@ -0,0 +1,2 @@
1
+ from .handler import Handler
2
+ from .handler import AsyncHandler
@@ -0,0 +1,109 @@
1
+ import abc
2
+ import datetime
3
+ from typing import List, Optional, Tuple, Union
4
+ from sqlalchemy.orm import Session, Query
5
+ from sqlalchemy.ext.asyncio import AsyncSession
6
+ from missionpanel.orm import Mission, Tag, MissionTag, Attempt
7
+ from sqlalchemy import select, func, distinct, case, Select
8
+
9
+
10
+ class HandlerInterface(abc.ABC):
11
+
12
+ @staticmethod
13
+ def query_missions_by_tag(tags: List[str]) -> Select[Tuple[Mission]]:
14
+ return (
15
+ select(Mission)
16
+ .join(MissionTag)
17
+ .join(Tag)
18
+ .filter(Tag.name.in_(tags))
19
+ .group_by(Mission.id)
20
+ .having(func.count(distinct(Tag.name)) == len(tags))
21
+ )
22
+
23
+ @staticmethod
24
+ def query_todo_missions(tags: List[str]) -> Select[Tuple[Mission]]:
25
+ return (
26
+ HandlerInterface.query_missions_by_tag(tags)
27
+ .outerjoin(Attempt)
28
+ .group_by(Mission.id)
29
+ .having(func.count(
30
+ case((( # see if Attempt is finished or working on the Mission
31
+ Attempt.success.is_(True) | # have finished handler
32
+ (Attempt.last_update_time + Attempt.max_time_interval >= datetime.datetime.now()) # have working handler
33
+ ), 0), else_=None)) <= 0) # get those Missions that have no finished or working Attempt
34
+ )
35
+
36
+ @staticmethod
37
+ def create_attempt(session: Union[Session | AsyncSession], mission: Mission, name: str, max_time_interval: datetime.timedelta = datetime.timedelta(seconds=1)) -> Attempt:
38
+ attempt = Attempt(
39
+ handler=name,
40
+ max_time_interval=max_time_interval,
41
+ content=mission.content,
42
+ mission=mission)
43
+ session.add(attempt)
44
+ return attempt
45
+
46
+
47
+ class Handler(HandlerInterface, abc.ABC):
48
+ def __init__(self, session: Session, name: str, max_time_interval: datetime.timedelta = datetime.timedelta(seconds=1)):
49
+ self.session = session
50
+ self.name = name
51
+ self.max_time_interval = max_time_interval
52
+
53
+ @abc.abstractmethod
54
+ def select_mission(self, missions: Query[Mission]) -> Optional[Mission]:
55
+ return missions[0] if missions else None
56
+
57
+ def report_attempt(self, mission: Mission, attempt: Attempt):
58
+ attempt.success = False
59
+ attempt.last_update_time = datetime.datetime.now()
60
+ self.session.commit()
61
+
62
+ @abc.abstractmethod
63
+ def execute_mission(self, mission: Mission, attempt: Attempt) -> bool:
64
+ pass
65
+
66
+ def run_once(self, tags: List[str]):
67
+ missions = self.session.execute(HandlerInterface.query_todo_missions(tags)).scalars().all()
68
+ mission = self.select_mission(missions)
69
+ if mission is None:
70
+ return
71
+ attempt = HandlerInterface.create_attempt(self.session, mission, self.name, self.max_time_interval)
72
+ self.report_attempt(mission, attempt)
73
+ if self.execute_mission(mission, attempt):
74
+ attempt.success = True
75
+ self.report_attempt(mission, attempt)
76
+ return attempt
77
+
78
+
79
+ class AsyncHandler(HandlerInterface, abc.ABC):
80
+ def __init__(self, session: AsyncSession, name: str, max_time_interval: datetime.timedelta = datetime.timedelta(seconds=1)):
81
+ self.session = session
82
+ self.name = name
83
+ self.max_time_interval = max_time_interval
84
+
85
+ @abc.abstractmethod
86
+ async def select_mission(self, missions: Query[Mission]) -> Optional[Mission]:
87
+ return missions[0] if missions else None
88
+
89
+ async def report_attempt(self, mission: Mission, attempt: Attempt):
90
+ attempt.last_update_time = datetime.datetime.now()
91
+ await self.session.commit()
92
+ await self.session.refresh(attempt)
93
+ await self.session.refresh(mission)
94
+
95
+ @abc.abstractmethod
96
+ async def execute_mission(self, mission: Mission, attempt: Attempt) -> bool:
97
+ pass
98
+
99
+ async def run_once(self, tags: List[str]):
100
+ missions = (await self.session.execute(HandlerInterface.query_todo_missions(tags))).scalars().all()
101
+ mission = await self.select_mission(missions)
102
+ if mission is None:
103
+ return
104
+ attempt = HandlerInterface.create_attempt(self.session, mission, self.name, self.max_time_interval)
105
+ await self.report_attempt(mission, attempt)
106
+ if await self.execute_mission(mission, attempt):
107
+ attempt.success = True
108
+ await self.report_attempt(mission, attempt)
109
+ return attempt
@@ -0,0 +1,2 @@
1
+ from .core import Base, Mission, Tag, MissionTag, Matcher
2
+ from .handler import Attempt
@@ -0,0 +1,69 @@
1
+ import datetime
2
+ from typing import List
3
+ from sqlalchemy import (
4
+ Column,
5
+ Integer,
6
+ Text,
7
+ JSON,
8
+ DateTime,
9
+ ForeignKey
10
+ )
11
+ from sqlalchemy.orm import relationship, DeclarativeBase, Mapped
12
+ from sqlalchemy.ext.asyncio import AsyncAttrs
13
+
14
+
15
+ class Base(AsyncAttrs, DeclarativeBase):
16
+ pass
17
+
18
+
19
+ class Mission(Base):
20
+ __tablename__ = "mission"
21
+ id = Column(Integer, primary_key=True, autoincrement=True, comment="Mission ID")
22
+ content = Column(JSON, default={}, comment="Mission Content")
23
+ create_time = Column(DateTime, default=datetime.datetime.now, comment="Mission Create Time")
24
+ last_update_time = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment="Mission Update Time")
25
+
26
+ # back populate relationships
27
+ matchers: Mapped[List['Matcher']] = relationship(back_populates="mission")
28
+ tags: Mapped[List['MissionTag']] = relationship(back_populates="mission")
29
+
30
+ def __repr__(self):
31
+ return f"Mission(id={self.id}, content={self.content.__repr__()}, create_time={self.create_time.__repr__()}, last_update_time={self.last_update_time.__repr__()})"
32
+
33
+
34
+ class Matcher(Base):
35
+ __tablename__ = "matcher"
36
+ pattern = Column(Text, primary_key=True, comment="Matcher Content")
37
+
38
+ # relationship
39
+ mission_id = Column(Integer, ForeignKey("mission.id"), index=True, comment="Mission ID")
40
+ mission: Mapped['Mission'] = relationship(Mission, back_populates="matchers")
41
+
42
+ def __repr__(self):
43
+ return f"Matcher(pattern={self.pattern.__repr__()}, mission_id={self.mission_id})"
44
+
45
+
46
+ class Tag(Base):
47
+ __tablename__ = "tag"
48
+ name = Column(Text, primary_key=True, comment="Tag Name")
49
+
50
+ # back populate relationships
51
+ missions: Mapped[List['MissionTag']] = relationship(back_populates="tag")
52
+
53
+ def __repr__(self):
54
+ return f"Tag(name={self.name.__repr__()})"
55
+
56
+
57
+ class MissionTag(Base):
58
+ __tablename__ = "missiontag"
59
+
60
+ # relationship
61
+ tag_name = Column(Text, ForeignKey("tag.name"), primary_key=True, comment="Tag")
62
+ tag: Mapped['Tag'] = relationship(Tag, back_populates="missions")
63
+
64
+ # relationship
65
+ mission_id = Column(Integer, ForeignKey("mission.id"), primary_key=True, comment="Mission ID")
66
+ mission: Mapped['Mission'] = relationship(Mission, back_populates="tags")
67
+
68
+ def __repr__(self):
69
+ return f"MissionTag(tag_name={self.tag_name.__repr__()}, mission_id={self.mission_id})"
@@ -0,0 +1,31 @@
1
+ import datetime
2
+ from sqlalchemy import (
3
+ Column,
4
+ Integer,
5
+ Text,
6
+ JSON,
7
+ Boolean,
8
+ DateTime,
9
+ Interval,
10
+ ForeignKey
11
+ )
12
+ from sqlalchemy.orm import relationship, Mapped
13
+ from .core import Base, Mission
14
+
15
+
16
+ class Attempt(Base):
17
+ __tablename__ = "attempt"
18
+ id = Column(Integer, primary_key=True, autoincrement=True, comment="Mission ID")
19
+ handler = Column(Text, comment="Handler Name")
20
+ create_time = Column(DateTime, default=datetime.datetime.now, comment="Attempt Start Time")
21
+ last_update_time = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment="Attempt Last Update Time")
22
+ max_time_interval = Column(Interval, default=datetime.timedelta(seconds=1), comment="Attempt Update Time Interval")
23
+ content = Column(JSON, default={}, comment="Mission Content at that time")
24
+ success = Column(Boolean, default=False, comment="If this Attempt has succeed")
25
+
26
+ # relationship
27
+ mission_id = Column(Integer, ForeignKey("mission.id"), comment="Mission ID")
28
+ mission: Mapped['Mission'] = relationship(Mission, backref="attempts")
29
+
30
+ def __repr__(self):
31
+ return f"Attempt(id={self.id}, handler={self.handler.__repr__()}, create_time={self.create_time.__repr__()}, last_update_time={self.last_update_time.__repr__()}, max_time_interval={self.max_time_interval.__repr__()}, content={self.content.__repr__()}, success={self.success}, mission_id={self.mission_id})"
@@ -0,0 +1,2 @@
1
+ from .submitter import Submitter
2
+ from .asynchronous import AsyncSubmitter
@@ -0,0 +1,45 @@
1
+ from typing import List, Union, Tuple
2
+ from sqlalchemy.orm import Session
3
+ from sqlalchemy.ext.asyncio import AsyncSession
4
+ from missionpanel.orm import Mission, Tag, Matcher, MissionTag
5
+ from sqlalchemy import select, Select
6
+
7
+
8
+ class SubmitterInterface:
9
+ '''
10
+ SubmitterInterface is an interface for Submitter and AsyncSubmitter to implement the common methods.
11
+ There is no anything about session.execute in this class.
12
+ '''
13
+ @staticmethod
14
+ def query_matcher(match_patterns: List[str]) -> Select[Tuple[Matcher]]:
15
+ return select(Matcher).where(Matcher.pattern.in_(match_patterns)).limit(1).with_for_update()
16
+
17
+ @staticmethod
18
+ def add_mission_matchers(session: Union[Session | AsyncSession], mission: Mission, match_patterns: List[str], existing_matchers: List[Matcher] = []) -> Mission:
19
+ exist_patterns = [matcher.pattern for matcher in existing_matchers]
20
+ session.add_all([Matcher(pattern=pattern, mission=mission) for pattern in match_patterns if pattern not in exist_patterns])
21
+
22
+ @staticmethod
23
+ def create_mission(session: Union[Session | AsyncSession], content: str, match_patterns: List[str], existing_mission: Union[Mission | None] = None) -> Mission:
24
+ if existing_mission is None:
25
+ mission = Mission(
26
+ content=content,
27
+ matchers=[Matcher(pattern=pattern) for pattern in match_patterns],
28
+ )
29
+ session.add(mission)
30
+ else:
31
+ mission = existing_mission
32
+ if mission.content != content:
33
+ mission.content = content
34
+ return mission
35
+
36
+ @staticmethod
37
+ def query_tag(tags_name: List[str]) -> Select[Tuple[Matcher]]:
38
+ return select(Tag).where(Tag.name.in_(tags_name)).with_for_update()
39
+
40
+ @staticmethod
41
+ def add_mission_tags(session: Union[Session | AsyncSession], mission: Mission, tags_name: List[str], exist_tags: List[Tag] = [], exist_mission_tags: List[MissionTag] = []):
42
+ exist_tags_name = [tag.name for tag in exist_tags]
43
+ session.add_all([Tag(name=tag_name) for tag_name in tags_name if tag_name not in exist_tags_name])
44
+ exist_mission_tags_name = [tag.tag_name for tag in exist_mission_tags]
45
+ session.add_all([MissionTag(mission=mission, tag_name=tag_name) for tag_name in tags_name if tag_name not in exist_mission_tags_name])
@@ -0,0 +1,60 @@
1
+ from typing import List, Union
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from missionpanel.orm import Mission
4
+ from .abc import SubmitterInterface
5
+
6
+
7
+ class AsyncSubmitterInterface(SubmitterInterface):
8
+
9
+ @staticmethod
10
+ async def _query_mission(session: AsyncSession, match_patterns: List[str]) -> Mission:
11
+ matcher = (await session.execute(SubmitterInterface.query_matcher(match_patterns))).scalars().first()
12
+ if matcher is None:
13
+ return None
14
+ mission = await matcher.awaitable_attrs.mission
15
+ existing_matchers = await mission.awaitable_attrs.matchers
16
+ SubmitterInterface.add_mission_matchers(session, mission, match_patterns, existing_matchers)
17
+ return mission
18
+
19
+ @staticmethod
20
+ async def _add_tags(session: AsyncSession, mission: Union[Mission | None] = None, tags: List[str] = []):
21
+ exist_tags = (await session.execute(SubmitterInterface.query_tag(tags))).scalars().all() if len(tags) > 0 else []
22
+ exist_mission_tags = await mission.awaitable_attrs.tags
23
+ SubmitterInterface.add_mission_tags(session, mission, tags, exist_tags, exist_mission_tags)
24
+
25
+ @staticmethod
26
+ async def match_mission(session: AsyncSession, match_patterns: List[str]) -> Mission:
27
+ mission = await AsyncSubmitterInterface._query_mission(session, match_patterns)
28
+ await session.commit()
29
+ return mission
30
+
31
+ @staticmethod
32
+ async def create_mission(session: AsyncSession, content: str, match_patterns: List[str], tags: List[str] = []):
33
+ mission = await AsyncSubmitterInterface._query_mission(session, match_patterns)
34
+ mission = SubmitterInterface.create_mission(session, content, match_patterns, mission)
35
+ await AsyncSubmitterInterface._add_tags(session, mission, tags)
36
+ await session.commit()
37
+ await session.refresh(mission)
38
+ return mission
39
+
40
+ @staticmethod
41
+ async def add_tags(session: AsyncSession, match_patterns: List[str], tags: List[str]):
42
+ mission = await AsyncSubmitterInterface._query_mission(session, match_patterns)
43
+ if mission is None:
44
+ raise ValueError("Mission not found")
45
+ await AsyncSubmitterInterface._add_tags(session, mission, tags)
46
+ await session.commit()
47
+
48
+
49
+ class AsyncSubmitter(AsyncSubmitterInterface):
50
+ def __init__(self, session: AsyncSession):
51
+ self.session = session
52
+
53
+ async def match_mission(self, match_patterns: List[str]) -> Mission:
54
+ return await AsyncSubmitterInterface.match_mission(self.session, match_patterns)
55
+
56
+ async def create_mission(self, content: str, match_patterns: List[str]):
57
+ return await AsyncSubmitterInterface.create_mission(self.session, content, match_patterns)
58
+
59
+ async def add_tags(self, matchers: List[str], tags: List[str]):
60
+ return await AsyncSubmitterInterface.add_tags(self.session, matchers, tags)
@@ -0,0 +1,56 @@
1
+ from typing import List, Union
2
+ from sqlalchemy.orm import Session
3
+ from missionpanel.orm import Mission
4
+ from .abc import SubmitterInterface
5
+
6
+
7
+ class SyncSubmitterInterface(SubmitterInterface):
8
+
9
+ @staticmethod
10
+ def _query_mission(session: Session, match_patterns: List[str]) -> Mission:
11
+ matcher = session.execute(SubmitterInterface.query_matcher(match_patterns)).scalars().first()
12
+ if matcher is None:
13
+ return None
14
+ SubmitterInterface.add_mission_matchers(session, matcher.mission, match_patterns, matcher.mission.matchers)
15
+ return matcher.mission
16
+
17
+ @staticmethod
18
+ def _add_tags(session: Session, mission: Union[Mission | None] = None, tags: List[str] = []):
19
+ exist_tags = session.execute(SubmitterInterface.query_tag(tags)).scalars().all()
20
+ tags = SubmitterInterface.add_mission_tags(session, mission, tags, exist_tags, mission.tags)
21
+
22
+ @staticmethod
23
+ def match_mission(session: Session, match_patterns: List[str]) -> Mission:
24
+ mission = SyncSubmitterInterface._query_mission(session, match_patterns)
25
+ session.commit()
26
+ return mission
27
+
28
+ @staticmethod
29
+ def create_mission(session: Session, content: str, match_patterns: List[str], tags: List[str] = []):
30
+ mission = SyncSubmitterInterface._query_mission(session, match_patterns)
31
+ mission = SubmitterInterface.create_mission(session, content, match_patterns, mission)
32
+ SyncSubmitterInterface._add_tags(session, mission, tags)
33
+ session.commit()
34
+ return mission
35
+
36
+ @staticmethod
37
+ def add_tags(session: Session, match_patterns: List[str], tags: List[str]):
38
+ mission = SyncSubmitterInterface._query_mission(session, match_patterns)
39
+ if mission is None:
40
+ raise ValueError("Mission not found")
41
+ SyncSubmitterInterface._add_tags(session, mission, tags)
42
+ session.commit()
43
+
44
+
45
+ class Submitter(SyncSubmitterInterface):
46
+ def __init__(self, session: Session):
47
+ self.session = session
48
+
49
+ def match_mission(self, match_patterns: List[str]) -> Mission:
50
+ return SyncSubmitterInterface.match_mission(self.session, match_patterns)
51
+
52
+ def create_mission(self, content: str, match_patterns: List[str], tags: List[str] = []):
53
+ return SyncSubmitterInterface.create_mission(self.session, content, match_patterns, tags)
54
+
55
+ def add_tags(self, match_patterns: List[str], tags: List[str]):
56
+ return SyncSubmitterInterface.add_tags(self.session, match_patterns, tags)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: missionpanel
3
- Version: 1.0.1
3
+ Version: 1.0.2
4
4
  Summary: A mission panel
5
5
  Home-page: https://github.com/yindaheng98/missionpanel
6
6
  Author: yindaheng98
@@ -0,0 +1,18 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ missionpanel/__init__.py
5
+ missionpanel.egg-info/PKG-INFO
6
+ missionpanel.egg-info/SOURCES.txt
7
+ missionpanel.egg-info/dependency_links.txt
8
+ missionpanel.egg-info/requires.txt
9
+ missionpanel.egg-info/top_level.txt
10
+ missionpanel/handler/__init__.py
11
+ missionpanel/handler/handler.py
12
+ missionpanel/orm/__init__.py
13
+ missionpanel/orm/core.py
14
+ missionpanel/orm/handler.py
15
+ missionpanel/submitter/__init__.py
16
+ missionpanel/submitter/abc.py
17
+ missionpanel/submitter/asynchronous.py
18
+ missionpanel/submitter/submitter.py
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python
2
2
  # coding: utf-8
3
3
 
4
- from setuptools import setup
4
+ from setuptools import find_packages, setup
5
5
 
6
6
  with open("README.md", "r", encoding='utf8') as fh:
7
7
  long_description = fh.read()
@@ -12,15 +12,15 @@ package_dir = {
12
12
 
13
13
  setup(
14
14
  name='missionpanel',
15
- version='1.0.1',
15
+ version='1.0.2',
16
16
  author='yindaheng98',
17
17
  author_email='yindaheng98@gmail.com',
18
18
  url='https://github.com/yindaheng98/missionpanel',
19
19
  description=u'A mission panel',
20
20
  long_description=long_description,
21
21
  long_description_content_type="text/markdown",
22
- package_dir=package_dir,
23
- packages=[key for key in package_dir],
22
+ package_dir={'missionpanel': 'missionpanel'},
23
+ packages=['missionpanel'] + ["missionpanel." + package for package in find_packages(where="missionpanel")],
24
24
  classifiers=[
25
25
  "Programming Language :: Python :: 3",
26
26
  "License :: OSI Approved :: MIT License",
@@ -1,9 +0,0 @@
1
- LICENSE
2
- README.md
3
- setup.py
4
- missionpanel/__init__.py
5
- missionpanel.egg-info/PKG-INFO
6
- missionpanel.egg-info/SOURCES.txt
7
- missionpanel.egg-info/dependency_links.txt
8
- missionpanel.egg-info/requires.txt
9
- missionpanel.egg-info/top_level.txt
File without changes
File without changes
File without changes