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.
Files changed (24) hide show
  1. {missionpanel-1.0.1 → missionpanel-1.1}/PKG-INFO +19 -16
  2. missionpanel-1.1/missionpanel/example/__init__.py +2 -0
  3. missionpanel-1.1/missionpanel/example/rsshub.py +46 -0
  4. missionpanel-1.1/missionpanel/example/ttrss.py +142 -0
  5. missionpanel-1.1/missionpanel/handler/__init__.py +2 -0
  6. missionpanel-1.1/missionpanel/handler/handler.py +109 -0
  7. missionpanel-1.1/missionpanel/orm/__init__.py +2 -0
  8. missionpanel-1.1/missionpanel/orm/core.py +69 -0
  9. missionpanel-1.1/missionpanel/orm/handler.py +31 -0
  10. missionpanel-1.1/missionpanel/submitter/__init__.py +2 -0
  11. missionpanel-1.1/missionpanel/submitter/abc.py +45 -0
  12. missionpanel-1.1/missionpanel/submitter/asynchronous.py +60 -0
  13. missionpanel-1.1/missionpanel/submitter/submitter.py +56 -0
  14. {missionpanel-1.0.1 → missionpanel-1.1}/missionpanel.egg-info/PKG-INFO +19 -16
  15. missionpanel-1.1/missionpanel.egg-info/SOURCES.txt +21 -0
  16. {missionpanel-1.0.1 → missionpanel-1.1}/setup.cfg +4 -4
  17. {missionpanel-1.0.1 → missionpanel-1.1}/setup.py +4 -4
  18. missionpanel-1.0.1/missionpanel.egg-info/SOURCES.txt +0 -9
  19. {missionpanel-1.0.1 → missionpanel-1.1}/LICENSE +0 -0
  20. {missionpanel-1.0.1 → missionpanel-1.1}/README.md +0 -0
  21. {missionpanel-1.0.1 → missionpanel-1.1}/missionpanel/__init__.py +0 -0
  22. {missionpanel-1.0.1 → missionpanel-1.1}/missionpanel.egg-info/dependency_links.txt +0 -0
  23. {missionpanel-1.0.1 → missionpanel-1.1}/missionpanel.egg-info/requires.txt +0 -0
  24. {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.0.1
4
- Summary: A mission panel
5
- Home-page: https://github.com/yindaheng98/missionpanel
6
- Author: yindaheng98
7
- Author-email: yindaheng98@gmail.com
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Operating System :: OS Independent
11
- Description-Content-Type: text/markdown
12
- License-File: LICENSE
13
-
14
- # missionnel
15
-
16
- Just a mission panel.
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,2 @@
1
+ from .rsshub import RSSHubSubmitter, RSSHubRootSubmitter, RSSHubSubitemSubmitter
2
+ from .ttrss import TTRSSClient, TTRSSSubmitter, TTRSSHubSubmitter, TTRRSSHubRootSubmitter, TTRRSSHubSubitemSubmitter
@@ -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,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,16 +1,19 @@
1
- Metadata-Version: 2.1
2
- Name: missionpanel
3
- Version: 1.0.1
4
- Summary: A mission panel
5
- Home-page: https://github.com/yindaheng98/missionpanel
6
- Author: yindaheng98
7
- Author-email: yindaheng98@gmail.com
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Operating System :: OS Independent
11
- Description-Content-Type: text/markdown
12
- License-File: LICENSE
13
-
14
- # missionnel
15
-
16
- Just a mission panel.
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.0.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=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