episoder 0.11.1__tar.gz → 1.0.0__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 (92) hide show
  1. episoder-1.0.0/PKG-INFO +110 -0
  2. episoder-1.0.0/README.md +77 -0
  3. episoder-1.0.0/episoder/config.py +102 -0
  4. episoder-1.0.0/episoder/database.py +132 -0
  5. {episoder-0.11.1 → episoder-1.0.0}/episoder/episoder.py +61 -78
  6. episoder-1.0.0/episoder/main.py +377 -0
  7. episoder-1.0.0/episoder/options.py +176 -0
  8. episoder-1.0.0/episoder/output.py +153 -0
  9. {episoder-0.11.1 → episoder-1.0.0}/episoder/sources/__init__.py +1 -1
  10. {episoder-0.11.1 → episoder-1.0.0}/episoder/sources/epguides.py +10 -8
  11. {episoder-0.11.1 → episoder-1.0.0}/episoder/sources/parser.py +4 -3
  12. {episoder-0.11.1 → episoder-1.0.0}/episoder/sources/thetvdb.py +18 -18
  13. {episoder-0.11.1 → episoder-1.0.0}/episoder/sources/tvcom.py +1 -1
  14. {episoder-0.11.1 → episoder-1.0.0}/episoder/version.py +2 -2
  15. episoder-1.0.0/episoder.egg-info/PKG-INFO +110 -0
  16. episoder-1.0.0/episoder.egg-info/SOURCES.txt +39 -0
  17. episoder-1.0.0/episoder.egg-info/requires.txt +14 -0
  18. {episoder-0.11.1 → episoder-1.0.0}/setup.cfg +11 -1
  19. episoder-1.0.0/test/test_commands.py +220 -0
  20. episoder-1.0.0/test/test_config.py +135 -0
  21. {episoder-0.11.1 → episoder-1.0.0}/test/test_database.py +140 -139
  22. {episoder-0.11.1 → episoder-1.0.0}/test/test_epguides_parser.py +113 -108
  23. {episoder-0.11.1 → episoder-1.0.0}/test/test_episode.py +12 -12
  24. episoder-1.0.0/test/test_main.py +911 -0
  25. {episoder-0.11.1 → episoder-1.0.0}/test/test_migration.py +22 -22
  26. episoder-1.0.0/test/test_notify.py +129 -0
  27. episoder-1.0.0/test/test_options.py +427 -0
  28. {episoder-0.11.1 → episoder-1.0.0}/test/test_output.py +61 -61
  29. {episoder-0.11.1 → episoder-1.0.0}/test/test_show.py +12 -12
  30. {episoder-0.11.1 → episoder-1.0.0}/test/test_tvdb.py +29 -29
  31. episoder-0.11.1/.gitignore +0 -2
  32. episoder-0.11.1/.gitlab-ci.yml +0 -124
  33. episoder-0.11.1/CHANGELOG +0 -222
  34. episoder-0.11.1/PKG-INFO +0 -71
  35. episoder-0.11.1/README.md +0 -47
  36. episoder-0.11.1/episoder/database.py +0 -134
  37. episoder-0.11.1/episoder/main.py +0 -449
  38. episoder-0.11.1/episoder/output.py +0 -169
  39. episoder-0.11.1/episoder.1 +0 -247
  40. episoder-0.11.1/episoder.egg-info/PKG-INFO +0 -71
  41. episoder-0.11.1/episoder.egg-info/SOURCES.txt +0 -76
  42. episoder-0.11.1/episoder.egg-info/requires.txt +0 -4
  43. episoder-0.11.1/examples/home.episoder +0 -30
  44. episoder-0.11.1/test/__init__.py +0 -0
  45. episoder-0.11.1/test/fixtures/epguides_48_hours_mistery.html +0 -399
  46. episoder-0.11.1/test/fixtures/epguides_american_idol.html +0 -872
  47. episoder-0.11.1/test/fixtures/epguides_bsg.html +0 -538
  48. episoder-0.11.1/test/fixtures/epguides_buzzcocks.html +0 -464
  49. episoder-0.11.1/test/fixtures/epguides_doctor_who.html +0 -842
  50. episoder-0.11.1/test/fixtures/epguides_eureka.html +0 -362
  51. episoder-0.11.1/test/fixtures/epguides_futurama.html +0 -407
  52. episoder-0.11.1/test/fixtures/epguides_house.html +0 -490
  53. episoder-0.11.1/test/fixtures/epguides_kr2008.html +0 -256
  54. episoder-0.11.1/test/fixtures/epguides_lost.html +0 -478
  55. episoder-0.11.1/test/fixtures/epguides_lost_html.html +0 -602
  56. episoder-0.11.1/test/fixtures/epguides_lower_decks.html +0 -340
  57. episoder-0.11.1/test/fixtures/epguides_midsomer_murders.html +0 -398
  58. episoder-0.11.1/test/fixtures/epguides_picard.html +0 -380
  59. episoder-0.11.1/test/fixtures/epguides_test_iso_8859_1.html +0 -138
  60. episoder-0.11.1/test/fixtures/tvdb_error_404.json +0 -3
  61. episoder-0.11.1/test/fixtures/tvdb_search_frasier.json +0 -10
  62. episoder-0.11.1/test/fixtures/tvdb_search_friends.json +0 -28
  63. episoder-0.11.1/test/fixtures/tvdb_test_show_260.json +0 -1
  64. episoder-0.11.1/test/fixtures/tvdb_test_show_260_1_eps.json +0 -19
  65. episoder-0.11.1/test/fixtures/tvdb_test_show_261.json +0 -1
  66. episoder-0.11.1/test/fixtures/tvdb_test_show_261_1_eps.json +0 -23
  67. episoder-0.11.1/test/fixtures/tvdb_test_show_261_2_eps.json +0 -23
  68. episoder-0.11.1/test/fixtures/tvdb_test_show_261_3_eps.json +0 -18
  69. episoder-0.11.1/test/fixtures/tvdb_test_show_262.json +0 -1
  70. episoder-0.11.1/test/fixtures/tvdb_test_show_262_1_eps.json +0 -20
  71. episoder-0.11.1/test/fixtures/tvdb_test_show_263.json +0 -1
  72. episoder-0.11.1/test/fixtures/tvdb_test_show_268156.json +0 -1
  73. episoder-0.11.1/test/fixtures/tvdb_test_show_268156_1_eps.json +0 -520
  74. episoder-0.11.1/test/fixtures/tvdb_test_show_295640.json +0 -1
  75. episoder-0.11.1/test/fixtures/tvdb_test_show_295640_1_eps.json +0 -1
  76. episoder-0.11.1/test/fixtures/tvdb_test_show_295640_2_eps.json +0 -1
  77. episoder-0.11.1/test/fixtures/tvdb_test_show_73739.json +0 -1
  78. episoder-0.11.1/test/fixtures/tvdb_test_show_73739_1_eps.json +0 -14
  79. episoder-0.11.1/test/fixtures/tvdb_test_show_75397.json +0 -1
  80. episoder-0.11.1/test/fixtures/tvdb_test_show_75397_1_eps.json +0 -1
  81. episoder-0.11.1/test/test_notify.py +0 -265
  82. episoder-0.11.1/test/util.py +0 -60
  83. {episoder-0.11.1 → episoder-1.0.0}/COPYING +0 -0
  84. {episoder-0.11.1 → episoder-1.0.0}/episoder/__init__.py +0 -0
  85. {episoder-0.11.1 → episoder-1.0.0}/episoder.egg-info/dependency_links.txt +0 -0
  86. {episoder-0.11.1 → episoder-1.0.0}/episoder.egg-info/entry_points.txt +0 -0
  87. {episoder-0.11.1 → episoder-1.0.0}/episoder.egg-info/top_level.txt +0 -0
  88. {episoder-0.11.1 → episoder-1.0.0}/pyproject.toml +0 -0
  89. {episoder-0.11.1 → episoder-1.0.0}/setup.py +0 -0
  90. {episoder-0.11.1 → episoder-1.0.0}/test/test_metainfo.py +0 -0
  91. {episoder-0.11.1 → episoder-1.0.0}/test/test_sources.py +0 -0
  92. {episoder-0.11.1 → episoder-1.0.0}/test/test_tvcom.py +0 -0
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: episoder
3
+ Version: 1.0.0
4
+ Summary: TV episode notifier
5
+ Home-page: https://code.ott.net/episoder/
6
+ Author: Stefan Ott
7
+ Author-email: stefan@ott.net
8
+ Project-URL: Source, https://git.ott.net/episoder/
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
13
+ Classifier: Natural Language :: English
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Topic :: Multimedia :: Video
17
+ Requires-Python: >=3.6
18
+ Description-Content-Type: text/markdown
19
+ License-File: COPYING
20
+ Requires-Dist: argparse
21
+ Requires-Dist: requests
22
+ Requires-Dist: beautifulsoup4
23
+ Requires-Dist: sqlalchemy>=2.0
24
+ Provides-Extra: test
25
+ Requires-Dist: mypy; extra == "test"
26
+ Requires-Dist: flake8; extra == "test"
27
+ Requires-Dist: pytest; extra == "test"
28
+ Requires-Dist: pyfakefs; extra == "test"
29
+ Requires-Dist: types-requests; extra == "test"
30
+ Provides-Extra: dev
31
+ Requires-Dist: setuptools; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # episoder
35
+
36
+ ![episoder](https://code.ott.net/episoder/screenshot.png "episoder 0.8.0")
37
+
38
+ Episoder is a command-line tool that tells you about new episodes of your
39
+ favourite TV shows. It gets its data from online TV episode guides such as
40
+ epguides.com and TheTVDB.
41
+
42
+ There is also a web frontend for episoder, called
43
+ [webisoder](https://code.ott.net/webisoder).
44
+
45
+ ## Dependencies
46
+
47
+ In order to use episoder, you need to have Python installed on your system. In
48
+ addition to the default distribution, episoder uses the `requests`, `argparse`,
49
+ `sqlalchemy` and `beautifulsoup4` packages.
50
+
51
+ ## Downloading
52
+
53
+ The official website, bug tracker etc. for episoder can be found at
54
+ [code.ott.net/episoder](https://code.ott.net/episoder/).
55
+
56
+ ## Installation
57
+
58
+ You can install episoder by running `pip install episoder` or you can download
59
+ the tarball from the official website, extract it and run `setup.py install`.
60
+
61
+ ## Getting started
62
+
63
+ First, find a show you're interested in:
64
+
65
+ $ episoder search firefly
66
+ ID Name
67
+ ------- --------------------
68
+ 78874 Firefly
69
+ 384248 Firefly Lane
70
+ 329088 Firefly (2017)
71
+
72
+ Add that show to episoder:
73
+
74
+ $ episoder add 78874
75
+
76
+ Update your database:
77
+
78
+ $ episoder update
79
+
80
+ And finally show your episodes:
81
+
82
+ $ episoder list
83
+
84
+ The list may be empty, by default episoder deletes old episodes and will only
85
+ show you the next few days. The `--nodate` option to the `update` and `list`
86
+ command will change that, in case you care about days long gone and/or far in
87
+ the future.
88
+
89
+ ## Configuration
90
+
91
+ The configuration file at `~/.episoder` contains your default settings for
92
+ episoder. After installing episoder, the man page will help you with the
93
+ configuration.
94
+
95
+ ## Using episoder
96
+
97
+ To use episoder you will typically:
98
+
99
+ * Create a cron job to update your database once a day using `episoder update`
100
+ * Add `episoder list` to your `~/.bashrc`, `~/.bash_profile`, `/etc/bash.bashrc`
101
+ or `/etc/profile` if you want to see your upcoming shows every time you start
102
+ a shell. Consult your shell's documentation to find out which file to use.
103
+ * Or run `episoder notify` at regular intervals to get notifications by e-mail.
104
+
105
+ ## More documentation
106
+
107
+ There is some documentation to help you get started:
108
+
109
+ * [Using episoder](https://code.ott.net/episoder/doc/use.html)
110
+ * [Episoder configuration](https://code.ott.net/episoder/doc/config.html)
@@ -0,0 +1,77 @@
1
+ # episoder
2
+
3
+ ![episoder](https://code.ott.net/episoder/screenshot.png "episoder 0.8.0")
4
+
5
+ Episoder is a command-line tool that tells you about new episodes of your
6
+ favourite TV shows. It gets its data from online TV episode guides such as
7
+ epguides.com and TheTVDB.
8
+
9
+ There is also a web frontend for episoder, called
10
+ [webisoder](https://code.ott.net/webisoder).
11
+
12
+ ## Dependencies
13
+
14
+ In order to use episoder, you need to have Python installed on your system. In
15
+ addition to the default distribution, episoder uses the `requests`, `argparse`,
16
+ `sqlalchemy` and `beautifulsoup4` packages.
17
+
18
+ ## Downloading
19
+
20
+ The official website, bug tracker etc. for episoder can be found at
21
+ [code.ott.net/episoder](https://code.ott.net/episoder/).
22
+
23
+ ## Installation
24
+
25
+ You can install episoder by running `pip install episoder` or you can download
26
+ the tarball from the official website, extract it and run `setup.py install`.
27
+
28
+ ## Getting started
29
+
30
+ First, find a show you're interested in:
31
+
32
+ $ episoder search firefly
33
+ ID Name
34
+ ------- --------------------
35
+ 78874 Firefly
36
+ 384248 Firefly Lane
37
+ 329088 Firefly (2017)
38
+
39
+ Add that show to episoder:
40
+
41
+ $ episoder add 78874
42
+
43
+ Update your database:
44
+
45
+ $ episoder update
46
+
47
+ And finally show your episodes:
48
+
49
+ $ episoder list
50
+
51
+ The list may be empty, by default episoder deletes old episodes and will only
52
+ show you the next few days. The `--nodate` option to the `update` and `list`
53
+ command will change that, in case you care about days long gone and/or far in
54
+ the future.
55
+
56
+ ## Configuration
57
+
58
+ The configuration file at `~/.episoder` contains your default settings for
59
+ episoder. After installing episoder, the man page will help you with the
60
+ configuration.
61
+
62
+ ## Using episoder
63
+
64
+ To use episoder you will typically:
65
+
66
+ * Create a cron job to update your database once a day using `episoder update`
67
+ * Add `episoder list` to your `~/.bashrc`, `~/.bash_profile`, `/etc/bash.bashrc`
68
+ or `/etc/profile` if you want to see your upcoming shows every time you start
69
+ a shell. Consult your shell's documentation to find out which file to use.
70
+ * Or run `episoder notify` at regular intervals to get notifications by e-mail.
71
+
72
+ ## More documentation
73
+
74
+ There is some documentation to help you get started:
75
+
76
+ * [Using episoder](https://code.ott.net/episoder/doc/use.html)
77
+ * [Episoder configuration](https://code.ott.net/episoder/doc/config.html)
@@ -0,0 +1,102 @@
1
+ # episoder, https://code.ott.net/episoder
2
+ #
3
+ # Copyright (C) 2004-2026 Stefan Ott. All rights reserved.
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ from logging import getLogger
19
+ from os import environ, path
20
+
21
+ from typing import BinaryIO, Optional
22
+
23
+ from .version import __version__
24
+
25
+
26
+ class InvalidDataFormatError(Exception):
27
+ def __init__(self, filename: str) -> None:
28
+ super().__init__(f'Please remove old/invalid data file: {filename}')
29
+
30
+
31
+ class EpisoderConfig:
32
+ # pylint: disable=too-few-public-methods,too-many-instance-attributes
33
+ def __init__(self) -> None:
34
+ self.agent = f'episoder/{__version__}'
35
+ self.datafile = path.join(environ['HOME'], '.episodes')
36
+ self.dateformat = '%a, %b %d, %Y'
37
+ self.format = '%airdate %show %seasonx%epnum'
38
+ self.tvdb_key = '8F15287C4B23B36E'
39
+
40
+ self.email_to: Optional[str] = None
41
+ self.email_username: Optional[str] = None
42
+ self.email_password: Optional[str] = None
43
+ self.email_server = 'localhost'
44
+ self.email_port = 587
45
+ self.email_tls = False
46
+
47
+
48
+ class ConfigLoader:
49
+ def __init__(self) -> None:
50
+ self._log = getLogger('Config')
51
+
52
+ def _load_rcfile(self, file_: BinaryIO) -> dict[str, str]:
53
+ def strip_comments(line: str) -> str:
54
+ return line.split('#')[0]
55
+
56
+ def valid(line: str) -> bool:
57
+ return '=' in line
58
+
59
+ self._log.info('Loading config file')
60
+ lines = file_.readlines()
61
+ decoded_lines = [x.decode('utf8').strip() for x in lines]
62
+ non_comments = map(strip_comments, decoded_lines)
63
+ valid_lines = filter(valid, non_comments)
64
+ return dict(line.split('=') for line in valid_lines)
65
+
66
+ def _update(self, cfg: EpisoderConfig, values: dict[str, str]) -> None:
67
+ cfg.agent = values.get('agent', cfg.agent)
68
+ cfg.datafile = values.get('data', cfg.datafile)
69
+ cfg.dateformat = values.get('dateformat', cfg.dateformat)
70
+ cfg.format = values.get('format', cfg.format)
71
+ cfg.tvdb_key = values.get('tvdb_key', cfg.tvdb_key)
72
+
73
+ cfg.email_to = values.get('email_to', cfg.email_to)
74
+ cfg.email_username = values.get('email_username', cfg.email_username)
75
+ cfg.email_password = values.get('email_password', cfg.email_password)
76
+ cfg.email_port = int(values.get('email_port', cfg.email_port))
77
+ cfg.email_server = values.get('email_server', cfg.email_server)
78
+ cfg.email_tls = bool(values.get('email_tls', cfg.email_tls))
79
+
80
+ def load(self, file: str) -> EpisoderConfig:
81
+ config = EpisoderConfig()
82
+
83
+ if path.exists(file):
84
+ with open(file, 'rb') as file_:
85
+ values = self._load_rcfile(file_)
86
+
87
+ self._log.debug('Settings loaded: %s', values)
88
+ self._update(config, values)
89
+ self._log.info("Loaded configuration")
90
+ else:
91
+ self._log.warning('No config file found, using defaults')
92
+
93
+ return config
94
+
95
+ def validate(self, config: EpisoderConfig) -> None:
96
+ filename = config.datafile
97
+ if path.exists(filename):
98
+ with open(filename, 'rb') as file:
99
+ data = file.read(6)
100
+
101
+ if data != b'SQLite':
102
+ raise InvalidDataFormatError(filename)
@@ -0,0 +1,132 @@
1
+ # episoder, https://code.ott.net/episoder
2
+ #
3
+ # Copyright (C) 2004-2026 Stefan Ott. All rights reserved.
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ from datetime import date, datetime
19
+ from typing import Any, Optional
20
+
21
+ from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Integer, Text
22
+ from sqlalchemy import Sequence
23
+ from sqlalchemy.orm import backref, DeclarativeBase, relationship
24
+ from sqlalchemy.orm import mapped_column
25
+
26
+
27
+ # pylint: disable=too-few-public-methods
28
+ class Base(DeclarativeBase):
29
+ pass
30
+
31
+
32
+ class Show(Base):
33
+ RUNNING = 1
34
+ SUSPENDED = 2
35
+ ENDED = 3
36
+
37
+ __tablename__ = "shows"
38
+
39
+ id = mapped_column("show_id", Integer, Sequence("shows_show_id_seq"),
40
+ primary_key=True)
41
+ name = mapped_column("show_name", Text)
42
+ url = mapped_column(Text, unique=True)
43
+ updated = mapped_column(DateTime)
44
+ enabled = mapped_column(Boolean)
45
+ status = mapped_column(Integer, default=RUNNING)
46
+
47
+ def __init__(self, name: str, url: str = "",
48
+ updated: datetime = datetime.fromtimestamp(0)) -> None:
49
+ super().__init__()
50
+ self.name = name
51
+ self.url = url
52
+ self.updated = updated
53
+ self.status = Show.RUNNING
54
+ self.episodes: list[Episode] = []
55
+ self.enabled = True
56
+
57
+ def __str__(self) -> str:
58
+ return f"Show: {self.name}"
59
+
60
+ def __repr__(self) -> str:
61
+ return f'Show("{self.name}", "{self.url}")'
62
+
63
+ def __eq__(self, other: object) -> Any:
64
+ if not isinstance(other, Show):
65
+ return NotImplemented
66
+
67
+ return self.name == other.name and self.url == other.url
68
+
69
+
70
+ # pylint: disable=too-many-instance-attributes
71
+ class Episode(Base):
72
+ __tablename__ = "episodes"
73
+
74
+ show_id = mapped_column(Integer, ForeignKey(Show.id), primary_key=True)
75
+ episode = mapped_column("num", Integer, primary_key=True)
76
+ airdate = mapped_column(Date)
77
+ season = mapped_column(Integer, primary_key=True)
78
+ title = mapped_column(Text)
79
+ totalnum = mapped_column(Integer)
80
+ prodnum = mapped_column(Text)
81
+ notified = mapped_column(Date)
82
+ show = relationship(Show, backref=backref("episodes", cascade="all"))
83
+
84
+ # pylint: disable=too-many-arguments,too-many-positional-arguments
85
+ def __init__(self, name: str, season: int, episode: int, aired: date,
86
+ prodnum: Optional[str], total: int) -> None:
87
+ super().__init__()
88
+ self.title = name
89
+ self.season = int(season)
90
+ self.episode = int(episode)
91
+ self.airdate = aired
92
+ self.prodnum = prodnum
93
+ self.totalnum = int(total)
94
+
95
+ def __str__(self) -> str:
96
+ return f"{self.show.name} {self.season}x{self.episode:02d}: " \
97
+ f"{self.title}"
98
+
99
+ def __repr__(self) -> str:
100
+ airdate = self.airdate
101
+
102
+ return f'Episode("{self.title}", {self.season}, {self.episode}, ' \
103
+ f'date({airdate.year}, {airdate.month}, {airdate.day}), ' \
104
+ f'"{self.prodnum}", {self.totalnum})'
105
+
106
+ def __lt__(self, other: object) -> Any:
107
+ if not isinstance(other, Episode):
108
+ return NotImplemented
109
+
110
+ return ((self.season < other.season)
111
+ or ((self.season == other.season)
112
+ and (self.episode < other.episode)))
113
+
114
+ def __eq__(self, other: object) -> Any:
115
+ if not isinstance(other, Episode):
116
+ return NotImplemented
117
+
118
+ return (self.show_id == other.show_id
119
+ and self.season == other.season
120
+ and self.episode == other.episode)
121
+
122
+
123
+ class Meta(Base):
124
+ __tablename__ = "meta"
125
+ key = mapped_column(Text, primary_key=True)
126
+ value = mapped_column(Text)
127
+
128
+ def __str__(self) -> str:
129
+ return f"Meta: {self.key} = {self.value}"
130
+
131
+ def __repr__(self) -> str:
132
+ return str(self)