ilpost-api-wrapper 0.1.0__tar.gz → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ilpost-api-wrapper
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: Python wrapper for Il Post newspaper API
5
5
  Author: Antonio Girasella
6
6
  License-Expression: MIT
@@ -166,7 +166,7 @@ for group in result.filters:
166
166
 
167
167
  ## CLI
168
168
 
169
- A command-line interface is provided via `main.py`:
169
+ The `ilpost-search` command is included with the package:
170
170
 
171
171
  ```
172
172
  usage: ilpost-search [-h] [--type {articles,podcasts,newsletters}]
@@ -179,19 +179,19 @@ usage: ilpost-search [-h] [--type {articles,podcasts,newsletters}]
179
179
 
180
180
  ```bash
181
181
  # Basic search
182
- python main.py berlusconi
182
+ ilpost-search berlusconi
183
183
 
184
184
  # Most recent articles in politics
185
- python main.py renzi --type articles --sort newest --category politica
185
+ ilpost-search renzi --type articles --sort newest --category politica
186
186
 
187
187
  # Podcast search, past 30 days
188
- python main.py cacao --type podcasts --date month
188
+ ilpost-search cacao --type podcasts --date month
189
189
 
190
190
  # Page 2, 5 results per page, oldest first
191
- python main.py sicilia --sort oldest --hits 5 --page 2
191
+ ilpost-search sicilia --sort oldest --hits 5 --page 2
192
192
 
193
193
  # Fetch all pages of newsletter results (up to 3 pages)
194
- python main.py economia --type newsletters --all-pages --max-pages 3
194
+ ilpost-search economia --type newsletters --all-pages --max-pages 3
195
195
  ```
196
196
 
197
197
  ## Notes
@@ -140,7 +140,7 @@ for group in result.filters:
140
140
 
141
141
  ## CLI
142
142
 
143
- A command-line interface is provided via `main.py`:
143
+ The `ilpost-search` command is included with the package:
144
144
 
145
145
  ```
146
146
  usage: ilpost-search [-h] [--type {articles,podcasts,newsletters}]
@@ -153,19 +153,19 @@ usage: ilpost-search [-h] [--type {articles,podcasts,newsletters}]
153
153
 
154
154
  ```bash
155
155
  # Basic search
156
- python main.py berlusconi
156
+ ilpost-search berlusconi
157
157
 
158
158
  # Most recent articles in politics
159
- python main.py renzi --type articles --sort newest --category politica
159
+ ilpost-search renzi --type articles --sort newest --category politica
160
160
 
161
161
  # Podcast search, past 30 days
162
- python main.py cacao --type podcasts --date month
162
+ ilpost-search cacao --type podcasts --date month
163
163
 
164
164
  # Page 2, 5 results per page, oldest first
165
- python main.py sicilia --sort oldest --hits 5 --page 2
165
+ ilpost-search sicilia --sort oldest --hits 5 --page 2
166
166
 
167
167
  # Fetch all pages of newsletter results (up to 3 pages)
168
- python main.py economia --type newsletters --all-pages --max-pages 3
168
+ ilpost-search economia --type newsletters --all-pages --max-pages 3
169
169
  ```
170
170
 
171
171
  ## Notes
@@ -0,0 +1,164 @@
1
+ import argparse
2
+ import sys
3
+
4
+ from .client import IlPostClient
5
+ from .models import SortOrder, ContentType, DateRange
6
+
7
+
8
+ def build_parser() -> argparse.ArgumentParser:
9
+ parser = argparse.ArgumentParser(
10
+ prog="ilpost-search",
11
+ description="Search Il Post articles, podcasts, and newsletters.",
12
+ )
13
+
14
+ parser.add_argument("query", help="Search term")
15
+
16
+ parser.add_argument(
17
+ "--type", "-t",
18
+ dest="content_type",
19
+ choices=["articles", "podcasts", "newsletters"],
20
+ default=None,
21
+ help="Filter by content type (default: all)",
22
+ )
23
+ parser.add_argument(
24
+ "--sort", "-s",
25
+ choices=["relevance", "newest", "oldest"],
26
+ default="relevance",
27
+ help="Sort order (default: relevance)",
28
+ )
29
+ parser.add_argument(
30
+ "--date", "-d",
31
+ dest="date_range",
32
+ choices=["all", "year", "month"],
33
+ default=None,
34
+ help="Publication date filter: all / year (past 12 months) / month (past 30 days)",
35
+ )
36
+ parser.add_argument(
37
+ "--category", "-c",
38
+ default=None,
39
+ metavar="CATEGORY",
40
+ help="Editorial category, e.g. politica, cultura, economia (articles only)",
41
+ )
42
+ parser.add_argument(
43
+ "--page", "-p",
44
+ type=int,
45
+ default=1,
46
+ help="Page number, 1-based (default: 1)",
47
+ )
48
+ parser.add_argument(
49
+ "--hits", "-n",
50
+ type=int,
51
+ default=10,
52
+ help="Results per page (default: 10)",
53
+ )
54
+ parser.add_argument(
55
+ "--all-pages",
56
+ action="store_true",
57
+ help="Fetch and print all pages (ignores --page)",
58
+ )
59
+ parser.add_argument(
60
+ "--max-pages",
61
+ type=int,
62
+ default=None,
63
+ metavar="N",
64
+ help="Maximum number of pages to fetch when using --all-pages",
65
+ )
66
+
67
+ return parser
68
+
69
+
70
+ _SORT_MAP = {
71
+ "relevance": SortOrder.RELEVANCE,
72
+ "newest": SortOrder.NEWEST,
73
+ "oldest": SortOrder.OLDEST,
74
+ }
75
+
76
+ _CTYPE_MAP = {
77
+ "articles": ContentType.ARTICLES,
78
+ "podcasts": ContentType.PODCASTS,
79
+ "newsletters": ContentType.NEWSLETTERS,
80
+ }
81
+
82
+ _DATE_MAP = {
83
+ "all": DateRange.ALL_TIME,
84
+ "year": DateRange.PAST_YEAR,
85
+ "month": DateRange.PAST_30_DAYS,
86
+ }
87
+
88
+ _TYPE_LABEL = {
89
+ "post": "article",
90
+ "episodes": "podcast",
91
+ "newsletter": "newsletter",
92
+ }
93
+
94
+
95
+ def print_result(result, *, show_header: bool = True) -> None:
96
+ if show_header:
97
+ print(f'\nQuery: "{result.query}" | '
98
+ f"Total: {result.total} | "
99
+ f"Page: {result.page}/{result.total_pages} | "
100
+ f"Sort: {result.sort}")
101
+ print("-" * 72)
102
+
103
+ if not result.docs:
104
+ print("No results.")
105
+ return
106
+
107
+ for doc in result.docs:
108
+ label = _TYPE_LABEL.get(doc.type, doc.type)
109
+ paywall = " [subscribers only]" if doc.is_paywalled else ""
110
+ score = f" score={doc.score:.2f}" if doc.score else ""
111
+ category = f" [{doc.category}]" if doc.category else ""
112
+ print(f"[{label}]{category}{paywall}{score}")
113
+ print(f" {doc.title}")
114
+ print(f" {doc.link}")
115
+ if doc.highlight:
116
+ # strip HTML span tags for plain-text display
117
+ snippet = doc.highlight.replace("<span>", ">>").replace("</span>", "<<")
118
+ print(f" ...{snippet}...")
119
+ print()
120
+
121
+
122
+ def main() -> None:
123
+ parser = build_parser()
124
+ args = parser.parse_args()
125
+
126
+ sort = _SORT_MAP[args.sort]
127
+ content_type = _CTYPE_MAP.get(args.content_type)
128
+ date_range = _DATE_MAP.get(args.date_range)
129
+
130
+ client = IlPostClient()
131
+
132
+ try:
133
+ if args.all_pages:
134
+ first = True
135
+ for page_result in client.paginate(
136
+ args.query,
137
+ hits=args.hits,
138
+ sort=sort,
139
+ content_type=content_type,
140
+ category=args.category,
141
+ date_range=date_range,
142
+ max_pages=args.max_pages,
143
+ ):
144
+ print_result(page_result, show_header=first)
145
+ first = False
146
+ else:
147
+ result = client.search(
148
+ args.query,
149
+ page=args.page,
150
+ hits=args.hits,
151
+ sort=sort,
152
+ content_type=content_type,
153
+ category=args.category,
154
+ date_range=date_range,
155
+ )
156
+ print_result(result)
157
+
158
+ except Exception as exc:
159
+ print(f"Error: {exc}", file=sys.stderr)
160
+ sys.exit(1)
161
+
162
+
163
+ if __name__ == "__main__":
164
+ main()
@@ -13,17 +13,19 @@ _BASE_URL = "https://api.ilpost.org/search/api/site_search/"
13
13
 
14
14
  def _build_filters(
15
15
  content_type: Optional[ContentType] = None,
16
- category: Optional[str] = None,
16
+ category: Optional[Union[str, list[str]]] = None,
17
17
  date_range: Optional[DateRange] = None,
18
18
  ) -> str:
19
19
  parts: list[str] = []
20
20
  if content_type is not None:
21
21
  parts.append(f"ctype:{content_type.value}")
22
22
  if category is not None:
23
- parts.append(f"category:{category}")
23
+ cats = [category] if isinstance(category, str) else category
24
+ for cat in cats:
25
+ parts.append(f"category:{cat}")
24
26
  if date_range is not None:
25
27
  parts.append(f"pub_date:{date_range.value}")
26
- return ",".join(parts)
28
+ return ";".join(parts)
27
29
 
28
30
 
29
31
  class IlPostClient:
@@ -52,7 +54,7 @@ class IlPostClient:
52
54
  hits: int = 10,
53
55
  sort: Union[SortOrder, str] = SortOrder.RELEVANCE,
54
56
  content_type: Optional[ContentType] = None,
55
- category: Optional[str] = None,
57
+ category: Optional[Union[str, list[str]]] = None,
56
58
  date_range: Optional[DateRange] = None,
57
59
  filters: Optional[str] = None,
58
60
  ) -> SearchResult:
@@ -61,7 +63,18 @@ class IlPostClient:
61
63
  Parameters
62
64
  ----------
63
65
  query:
64
- Search term.
66
+ Search term. Supports:
67
+
68
+ - Exact phrase: ``'"goffredo fofi"'``
69
+ - Boolean OR: ``"fofi | berlusconi"`` (``|`` and ``OR`` both work)
70
+ - Boolean AND: ``"fofi AND berlusconi"`` or just ``"fofi berlusconi"``
71
+ - Boolean NOT: ``"berlusconi NOT fininvest"``
72
+
73
+ The following syntax does **not** work and should be avoided:
74
+
75
+ - Field prefix (``title:fofi``, ``content:fofi``) — treated as literal tokens
76
+ - Boost operator (``berlusconi^10``) — the numeric value becomes a search token
77
+ - Proximity queries (``"goffredo fofi"~5``) — inflates results unpredictably
65
78
  page:
66
79
  1-based page number (default: 1).
67
80
  hits:
@@ -73,15 +86,17 @@ class IlPostClient:
73
86
  Filter by content type: ``ContentType.ARTICLES``, ``ContentType.PODCASTS``,
74
87
  or ``ContentType.NEWSLETTERS``.
75
88
  category:
76
- Filter articles by editorial category (e.g. ``"politica"``, ``"cultura"``).
77
- Only meaningful when ``content_type=ContentType.ARTICLES`` or no content
78
- type filter is set.
89
+ Filter articles by editorial category. Pass a single string
90
+ (e.g. ``"politica"``) or a list to AND multiple categories together
91
+ (e.g. ``["cultura", "libri"]``). Only meaningful when
92
+ ``content_type=ContentType.ARTICLES`` or no content type filter is set.
79
93
  date_range:
80
94
  Filter by publication date: ``DateRange.ALL_TIME``, ``DateRange.PAST_YEAR``,
81
95
  or ``DateRange.PAST_30_DAYS``.
82
96
  filters:
83
- Raw pre-encoded filter string (e.g. ``"ctype:articoli,pub_date:ultimo_anno"``).
84
- When provided, overrides ``content_type``, ``category``, and ``date_range``.
97
+ Raw pre-encoded filter string (e.g. ``"ctype:articoli;pub_date:ultimo_anno"``).
98
+ Filters are separated by ``;``. When provided, overrides ``content_type``,
99
+ ``category``, and ``date_range``.
85
100
 
86
101
  Returns
87
102
  -------
@@ -110,7 +125,7 @@ class IlPostClient:
110
125
  page: int = 1,
111
126
  hits: int = 10,
112
127
  sort: Union[SortOrder, str] = SortOrder.RELEVANCE,
113
- category: Optional[str] = None,
128
+ category: Optional[Union[str, list[str]]] = None,
114
129
  date_range: Optional[DateRange] = None,
115
130
  ) -> SearchResult:
116
131
  """Search articles only. Convenience wrapper around :meth:`search`."""
@@ -169,7 +184,7 @@ class IlPostClient:
169
184
  hits: int = 10,
170
185
  sort: Union[SortOrder, str] = SortOrder.RELEVANCE,
171
186
  content_type: Optional[ContentType] = None,
172
- category: Optional[str] = None,
187
+ category: Optional[Union[str, list[str]]] = None,
173
188
  date_range: Optional[DateRange] = None,
174
189
  max_pages: Optional[int] = None,
175
190
  ):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ilpost-api-wrapper
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: Python wrapper for Il Post newspaper API
5
5
  Author: Antonio Girasella
6
6
  License-Expression: MIT
@@ -166,7 +166,7 @@ for group in result.filters:
166
166
 
167
167
  ## CLI
168
168
 
169
- A command-line interface is provided via `main.py`:
169
+ The `ilpost-search` command is included with the package:
170
170
 
171
171
  ```
172
172
  usage: ilpost-search [-h] [--type {articles,podcasts,newsletters}]
@@ -179,19 +179,19 @@ usage: ilpost-search [-h] [--type {articles,podcasts,newsletters}]
179
179
 
180
180
  ```bash
181
181
  # Basic search
182
- python main.py berlusconi
182
+ ilpost-search berlusconi
183
183
 
184
184
  # Most recent articles in politics
185
- python main.py renzi --type articles --sort newest --category politica
185
+ ilpost-search renzi --type articles --sort newest --category politica
186
186
 
187
187
  # Podcast search, past 30 days
188
- python main.py cacao --type podcasts --date month
188
+ ilpost-search cacao --type podcasts --date month
189
189
 
190
190
  # Page 2, 5 results per page, oldest first
191
- python main.py sicilia --sort oldest --hits 5 --page 2
191
+ ilpost-search sicilia --sort oldest --hits 5 --page 2
192
192
 
193
193
  # Fetch all pages of newsletter results (up to 3 pages)
194
- python main.py economia --type newsletters --all-pages --max-pages 3
194
+ ilpost-search economia --type newsletters --all-pages --max-pages 3
195
195
  ```
196
196
 
197
197
  ## Notes
@@ -2,10 +2,12 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  ilpost/__init__.py
5
+ ilpost/cli.py
5
6
  ilpost/client.py
6
7
  ilpost/models.py
7
8
  ilpost/py.typed
8
9
  ilpost_api_wrapper.egg-info/PKG-INFO
9
10
  ilpost_api_wrapper.egg-info/SOURCES.txt
10
11
  ilpost_api_wrapper.egg-info/dependency_links.txt
12
+ ilpost_api_wrapper.egg-info/entry_points.txt
11
13
  ilpost_api_wrapper.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ilpost-search = ilpost.cli:main
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ilpost-api-wrapper"
7
- version = "0.1.0"
7
+ version = "0.3.0"
8
8
  description = "Python wrapper for Il Post newspaper API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -33,5 +33,8 @@ Homepage = "https://github.com/girasella/ilpost-api-wrapper"
33
33
  Repository = "https://github.com/girasella/ilpost-api-wrapper"
34
34
  Issues = "https://github.com/girasella/ilpost-api-wrapper/issues"
35
35
 
36
+ [project.scripts]
37
+ ilpost-search = "ilpost.cli:main"
38
+
36
39
  [tool.setuptools.packages.find]
37
40
  where = ["."]