missionpanel 1.0.1__tar.gz → 1.1__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.
- {missionpanel-1.0.1 → missionpanel-1.1}/PKG-INFO +19 -16
- missionpanel-1.1/missionpanel/example/__init__.py +2 -0
- missionpanel-1.1/missionpanel/example/rsshub.py +46 -0
- missionpanel-1.1/missionpanel/example/ttrss.py +142 -0
- missionpanel-1.1/missionpanel/handler/__init__.py +2 -0
- missionpanel-1.1/missionpanel/handler/handler.py +109 -0
- missionpanel-1.1/missionpanel/orm/__init__.py +2 -0
- missionpanel-1.1/missionpanel/orm/core.py +69 -0
- missionpanel-1.1/missionpanel/orm/handler.py +31 -0
- missionpanel-1.1/missionpanel/submitter/__init__.py +2 -0
- missionpanel-1.1/missionpanel/submitter/abc.py +45 -0
- missionpanel-1.1/missionpanel/submitter/asynchronous.py +60 -0
- missionpanel-1.1/missionpanel/submitter/submitter.py +56 -0
- {missionpanel-1.0.1 → missionpanel-1.1}/missionpanel.egg-info/PKG-INFO +19 -16
- missionpanel-1.1/missionpanel.egg-info/SOURCES.txt +21 -0
- {missionpanel-1.0.1 → missionpanel-1.1}/setup.cfg +4 -4
- {missionpanel-1.0.1 → missionpanel-1.1}/setup.py +4 -4
- missionpanel-1.0.1/missionpanel.egg-info/SOURCES.txt +0 -9
- {missionpanel-1.0.1 → missionpanel-1.1}/LICENSE +0 -0
- {missionpanel-1.0.1 → missionpanel-1.1}/README.md +0 -0
- {missionpanel-1.0.1 → missionpanel-1.1}/missionpanel/__init__.py +0 -0
- {missionpanel-1.0.1 → missionpanel-1.1}/missionpanel.egg-info/dependency_links.txt +0 -0
- {missionpanel-1.0.1 → missionpanel-1.1}/missionpanel.egg-info/requires.txt +0 -0
- {missionpanel-1.0.1 → missionpanel-1.1}/missionpanel.egg-info/top_level.txt +0 -0
@@ -1,16 +1,19 @@
|
|
1
|
-
Metadata-Version: 2.1
|
2
|
-
Name: missionpanel
|
3
|
-
Version: 1.
|
4
|
-
Summary: A mission panel
|
5
|
-
Home-page: https://github.com/yindaheng98/missionpanel
|
6
|
-
Author: yindaheng98
|
7
|
-
Author-email: yindaheng98@gmail.com
|
8
|
-
|
9
|
-
|
10
|
-
Classifier:
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: missionpanel
|
3
|
+
Version: 1.1
|
4
|
+
Summary: A mission panel
|
5
|
+
Home-page: https://github.com/yindaheng98/missionpanel
|
6
|
+
Author: yindaheng98
|
7
|
+
Author-email: yindaheng98@gmail.com
|
8
|
+
License: UNKNOWN
|
9
|
+
Platform: UNKNOWN
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
12
|
+
Classifier: Operating System :: OS Independent
|
13
|
+
Description-Content-Type: text/markdown
|
14
|
+
License-File: LICENSE
|
15
|
+
|
16
|
+
# missionnel
|
17
|
+
|
18
|
+
Just a mission panel.
|
19
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import abc
|
2
|
+
from typing import AsyncGenerator, List, Any
|
3
|
+
import httpx
|
4
|
+
from xml.etree import ElementTree
|
5
|
+
from missionpanel.submitter import AsyncSubmitter
|
6
|
+
|
7
|
+
|
8
|
+
class RSSHubSubmitter(AsyncSubmitter, metaclass=abc.ABCMeta):
|
9
|
+
|
10
|
+
@abc.abstractmethod
|
11
|
+
async def parse_xml(self, xml: str) -> AsyncGenerator[Any, None]:
|
12
|
+
pass
|
13
|
+
|
14
|
+
async def derive_tags(self, mission_content) -> List[str]:
|
15
|
+
return []
|
16
|
+
|
17
|
+
async def derive_matcher(self, mission_content) -> List[str]:
|
18
|
+
return [mission_content['url']]
|
19
|
+
|
20
|
+
async def create_missions(self, rsshub: str, **httpx_client_options):
|
21
|
+
async with httpx.AsyncClient(**httpx_client_options) as client:
|
22
|
+
response = await client.get(rsshub)
|
23
|
+
async for mission_content in self.parse_xml(response.text):
|
24
|
+
matchers = await self.derive_matcher(mission_content)
|
25
|
+
await self.create_mission(mission_content, matchers)
|
26
|
+
tags = await self.derive_tags(mission_content)
|
27
|
+
if len(tags) > 0:
|
28
|
+
await self.add_tags(matchers, tags)
|
29
|
+
|
30
|
+
|
31
|
+
class RSSHubRootSubmitter(RSSHubSubmitter):
|
32
|
+
|
33
|
+
async def parse_xml(self, xml: str) -> AsyncGenerator[str, None]:
|
34
|
+
root = ElementTree.XML(xml)
|
35
|
+
yield {
|
36
|
+
'url': root.find('channel/link').text,
|
37
|
+
'latest': [item.find('link').text for item in root.iter('item')][0]
|
38
|
+
}
|
39
|
+
|
40
|
+
|
41
|
+
class RSSHubSubitemSubmitter(RSSHubSubmitter):
|
42
|
+
|
43
|
+
async def parse_xml(self, xml: str) -> AsyncGenerator[str, None]:
|
44
|
+
root = ElementTree.XML(xml)
|
45
|
+
for item in root.find('channel').iter('item'):
|
46
|
+
yield {'url': item.find('link').text}
|
@@ -0,0 +1,142 @@
|
|
1
|
+
import abc
|
2
|
+
import asyncio
|
3
|
+
import json
|
4
|
+
import logging
|
5
|
+
from typing import Any, AsyncGenerator, Dict, List
|
6
|
+
import httpx
|
7
|
+
from xml.etree import ElementTree
|
8
|
+
from missionpanel.submitter import AsyncSubmitter
|
9
|
+
|
10
|
+
|
11
|
+
class TTRSSClient(httpx.AsyncClient):
|
12
|
+
"""一个简单的异步TTRSS客户端"""
|
13
|
+
sem_list: Dict[str, asyncio.Semaphore] = {} # 同一时刻一个链接只能有一个客户端登录,这里用一个信号量列表控制
|
14
|
+
|
15
|
+
def __init__(self, url: str, username: str, password: str, **kwargs):
|
16
|
+
super().__init__(**kwargs)
|
17
|
+
self.__url = url
|
18
|
+
self.__username = username
|
19
|
+
self.__password = password
|
20
|
+
self.__logger = logging.getLogger("TTRSSClient")
|
21
|
+
self.__sid = None
|
22
|
+
if self.__url not in TTRSSClient.sem_list: # 给每个链接一个信号量
|
23
|
+
TTRSSClient.sem_list[self.__url] = None # 信号量必须在事件循环开始后生成,此处先给个标记
|
24
|
+
|
25
|
+
async def __aenter__(self):
|
26
|
+
for url in TTRSSClient.sem_list: # 信号量必须在事件循环开始后生成
|
27
|
+
if TTRSSClient.sem_list[url] is None: # 已经生成的信号量不要变
|
28
|
+
self.__logger.debug('semaphore for TTRSS API %s initialized' % url)
|
29
|
+
TTRSSClient.sem_list[url] = asyncio.Semaphore(1) # 生成信号量
|
30
|
+
await TTRSSClient.sem_list[self.__url].__aenter__() # 同一时刻一个链接只能有一个客户端登录
|
31
|
+
self.__logger.debug('semaphore for TTRSS API %s got' % self.__url)
|
32
|
+
await super().__aenter__()
|
33
|
+
self.__logger.debug('httpx cli for TTRSS API %s initialized' % self.__url)
|
34
|
+
try:
|
35
|
+
data = (await super().post(self.__url, content=json.dumps({
|
36
|
+
'op': 'login',
|
37
|
+
'user': self.__username,
|
38
|
+
'password': self.__password
|
39
|
+
}))).json()
|
40
|
+
self.__logger.debug('TTRSS API login response: %s' % data)
|
41
|
+
self.__sid = data['content']['session_id']
|
42
|
+
self.__logger.debug('TTRSS API login successful, sid: %s' % self.__sid)
|
43
|
+
except Exception:
|
44
|
+
self.__logger.exception('TTRSS API login failed, error: ')
|
45
|
+
return self
|
46
|
+
|
47
|
+
async def __aexit__(self, *args, **kwargs):
|
48
|
+
try:
|
49
|
+
data = (await super().post(self.__url, content=json.dumps({
|
50
|
+
"sid": self.__sid,
|
51
|
+
"op": "logout"
|
52
|
+
}))).json()
|
53
|
+
self.__logger.debug('TTRSS API logout response: %s' % data)
|
54
|
+
self.__logger.debug('TTRSS API logout successful, sid: %s' % self.__sid)
|
55
|
+
except Exception:
|
56
|
+
self.__logger.exception('TTRSS API logout failed, error: ')
|
57
|
+
await super().__aexit__(*args, **kwargs)
|
58
|
+
await TTRSSClient.sem_list[self.__url].__aexit__(*args, **kwargs)
|
59
|
+
self.__logger.debug('semaphore for TTRSS API %s released' % self.__url)
|
60
|
+
|
61
|
+
async def api(self, data: dict):
|
62
|
+
data['sid'] = self.__sid
|
63
|
+
self.__logger.debug("post data to TTRSS API %s: %s" % (self.__url, data))
|
64
|
+
try:
|
65
|
+
return (await super().post(self.__url, content=json.dumps(data))).json()['content']
|
66
|
+
except Exception:
|
67
|
+
self.__logger.exception('TTRSS API post failed, error: ')
|
68
|
+
return None
|
69
|
+
|
70
|
+
|
71
|
+
class TTRSSSubmitter(AsyncSubmitter, metaclass=abc.ABCMeta):
|
72
|
+
|
73
|
+
@abc.abstractmethod
|
74
|
+
async def parse_content(self, feed: dict, content: dict) -> AsyncGenerator[Any, None]:
|
75
|
+
pass
|
76
|
+
|
77
|
+
async def derive_tags(self, mission_content) -> List[str]:
|
78
|
+
return []
|
79
|
+
|
80
|
+
async def derive_matcher(self, mission_content) -> List[str]:
|
81
|
+
return [mission_content['url']]
|
82
|
+
|
83
|
+
async def create_missions(self, url: str, username: str, password: str, cat_id: int, **httpx_client_options):
|
84
|
+
async with TTRSSClient(url, username, password, **httpx_client_options) as client:
|
85
|
+
feeds = await client.api({
|
86
|
+
"op": "getFeeds",
|
87
|
+
"cat_id": cat_id,
|
88
|
+
"limit": None
|
89
|
+
})
|
90
|
+
for feed in feeds:
|
91
|
+
content = await client.api({
|
92
|
+
"op": "getHeadlines",
|
93
|
+
"feed_id": feed['id'],
|
94
|
+
"limit": 1,
|
95
|
+
"view_mode": "all_articles",
|
96
|
+
"order_by": "feed_dates"
|
97
|
+
})
|
98
|
+
async for mission_content in self.parse_content(feed, content, **httpx_client_options):
|
99
|
+
matchers = await self.derive_matcher(mission_content)
|
100
|
+
await self.create_mission(mission_content, matchers)
|
101
|
+
tags = await self.derive_tags(mission_content)
|
102
|
+
if len(tags) > 0:
|
103
|
+
await self.add_tags(matchers, tags)
|
104
|
+
|
105
|
+
|
106
|
+
class TTRSSHubSubmitter(TTRSSSubmitter):
|
107
|
+
logger = logging.getLogger("TTRSSHubSubmitter")
|
108
|
+
|
109
|
+
@abc.abstractmethod
|
110
|
+
async def parse_xml(self, xml: str) -> AsyncGenerator[Any, None]:
|
111
|
+
pass
|
112
|
+
|
113
|
+
async def parse_content_nocatch(self, feed: dict, content: dict, **httpx_client_options) -> AsyncGenerator[Any, None]:
|
114
|
+
async with httpx.AsyncClient(**httpx_client_options) as client:
|
115
|
+
response = await client.get(feed['feed_url'])
|
116
|
+
async for mission_content in self.parse_xml(response.text):
|
117
|
+
yield mission_content
|
118
|
+
|
119
|
+
async def parse_content(self, feed: dict, content: dict, **httpx_client_options) -> AsyncGenerator[Any, None]:
|
120
|
+
try:
|
121
|
+
async for mission_content in self.parse_content_nocatch(feed, content, **httpx_client_options):
|
122
|
+
yield mission_content
|
123
|
+
except Exception as e:
|
124
|
+
self.logger.warning(f'parse content failed, error: {e}')
|
125
|
+
|
126
|
+
|
127
|
+
class TTRRSSHubRootSubmitter(TTRSSHubSubmitter):
|
128
|
+
|
129
|
+
async def parse_xml(self, xml: str) -> AsyncGenerator[str, None]:
|
130
|
+
root = ElementTree.XML(xml)
|
131
|
+
yield {
|
132
|
+
'url': root.find('channel/link').text,
|
133
|
+
'latest': [item.find('link').text for item in root.iter('item')][0]
|
134
|
+
}
|
135
|
+
|
136
|
+
|
137
|
+
class TTRRSSHubSubitemSubmitter(TTRSSHubSubmitter):
|
138
|
+
|
139
|
+
async def parse_xml(self, xml: str) -> AsyncGenerator[str, None]:
|
140
|
+
root = ElementTree.XML(xml)
|
141
|
+
for item in root.find('channel').iter('item'):
|
142
|
+
yield {'url': item.find('link').text}
|
@@ -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,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,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,16 +1,19 @@
|
|
1
|
-
Metadata-Version: 2.1
|
2
|
-
Name: missionpanel
|
3
|
-
Version: 1.
|
4
|
-
Summary: A mission panel
|
5
|
-
Home-page: https://github.com/yindaheng98/missionpanel
|
6
|
-
Author: yindaheng98
|
7
|
-
Author-email: yindaheng98@gmail.com
|
8
|
-
|
9
|
-
|
10
|
-
Classifier:
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: missionpanel
|
3
|
+
Version: 1.1
|
4
|
+
Summary: A mission panel
|
5
|
+
Home-page: https://github.com/yindaheng98/missionpanel
|
6
|
+
Author: yindaheng98
|
7
|
+
Author-email: yindaheng98@gmail.com
|
8
|
+
License: UNKNOWN
|
9
|
+
Platform: UNKNOWN
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
12
|
+
Classifier: Operating System :: OS Independent
|
13
|
+
Description-Content-Type: text/markdown
|
14
|
+
License-File: LICENSE
|
15
|
+
|
16
|
+
# missionnel
|
17
|
+
|
18
|
+
Just a mission panel.
|
19
|
+
|
@@ -0,0 +1,21 @@
|
|
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/example/__init__.py
|
11
|
+
missionpanel/example/rsshub.py
|
12
|
+
missionpanel/example/ttrss.py
|
13
|
+
missionpanel/handler/__init__.py
|
14
|
+
missionpanel/handler/handler.py
|
15
|
+
missionpanel/orm/__init__.py
|
16
|
+
missionpanel/orm/core.py
|
17
|
+
missionpanel/orm/handler.py
|
18
|
+
missionpanel/submitter/__init__.py
|
19
|
+
missionpanel/submitter/abc.py
|
20
|
+
missionpanel/submitter/asynchronous.py
|
21
|
+
missionpanel/submitter/submitter.py
|
@@ -1,4 +1,4 @@
|
|
1
|
-
[egg_info]
|
2
|
-
tag_build =
|
3
|
-
tag_date = 0
|
4
|
-
|
1
|
+
[egg_info]
|
2
|
+
tag_build =
|
3
|
+
tag_date = 0
|
4
|
+
|
@@ -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.
|
15
|
+
version='1.1',
|
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=
|
23
|
-
packages=[
|
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",
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|