atpcli 0.1.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.
@@ -0,0 +1,14 @@
1
+ .venv/
2
+ env.ini
3
+ .ruff_cache/
4
+ .pytest_cache/
5
+ .idea/
6
+ __pycache__
7
+ .DS_Store
8
+ dist/
9
+ htmlcov/
10
+ *.coverage*
11
+ coverage.xml
12
+ .cache/
13
+ *.pyc
14
+ site/
@@ -0,0 +1 @@
1
+ 3.10
@@ -0,0 +1,95 @@
1
+ # Contributing
2
+
3
+ First things first: thank you for contributing! This project will be successful thanks to everyone who contributes, and we're happy to have you.
4
+
5
+ ## Bug or issue?
6
+
7
+ To raise a bug or issue please use [our GitHub](https://github.com/phalt/atpcli/issues).
8
+
9
+ Please check the issue has not been raised before by using the search feature.
10
+
11
+ When submitting an issue or bug, please make sure you provide thorough detail on:
12
+
13
+ 1. The version of atpcli you are using
14
+ 2. Any errors or outputs you see in your terminal
15
+ 3. Steps to reproduce the issue
16
+
17
+ ## Contribution
18
+
19
+ If you want to directly contribute you can do so in two ways:
20
+
21
+ 1. Documentation
22
+ 2. Code
23
+
24
+ ### Documentation
25
+
26
+ Fixing grammar, spelling mistakes, or expanding the documentation to cover features that are not yet documented, are all valuable contributions.
27
+
28
+ ### Code
29
+
30
+ Contribution by writing code for new features, or fixing bugs, is a great way to contribute to the project.
31
+
32
+ #### Set up
33
+
34
+ Clone the repo:
35
+
36
+ ```sh
37
+ git clone git@github.com:phalt/atpcli.git
38
+ cd atpcli
39
+ ```
40
+
41
+ Move to a feature branch:
42
+
43
+ ```sh
44
+ git branch -B my-branch-name
45
+ ```
46
+
47
+ Install UV (if not already installed):
48
+
49
+ ```sh
50
+ # On macOS and Linux:
51
+ curl -LsSf https://astral.sh/uv/install.sh | sh
52
+
53
+ # Or using pip:
54
+ pip install uv
55
+ ```
56
+
57
+ Install all the dependencies:
58
+
59
+ ```sh
60
+ make install
61
+ ```
62
+
63
+ This will use UV to create a virtual environment and install all dependencies. UV handles the virtual environment automatically, so you don't need to manually activate it.
64
+
65
+ To make sure you have things set up correctly, please run the tests:
66
+
67
+ ```sh
68
+ make test
69
+ ```
70
+
71
+ ### Preparing changes for review
72
+
73
+ Once you've made changes, here's a good checklist to run through before publishing for review:
74
+
75
+ Run tests:
76
+
77
+ ```sh
78
+ make test
79
+ ```
80
+
81
+ Format and lint the code:
82
+
83
+ ```sh
84
+ make format
85
+ ```
86
+
87
+ ### Making a pull request
88
+
89
+ Please push your changes up to a feature branch and make a new [pull request](https://github.com/phalt/atpcli/compare) on GitHub.
90
+
91
+ Please add a description to the PR and some information about why the change is being made.
92
+
93
+ After a review, you might need to make more changes.
94
+
95
+ Once accepted, a core contributor will merge your changes!
atpcli-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Paul Hallett
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
atpcli-0.1.0/Makefile ADDED
@@ -0,0 +1,37 @@
1
+ help:
2
+ @echo Developer commands for atpcli
3
+ @echo
4
+ @grep -E '^[ .a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
5
+ @echo
6
+
7
+ install: ## Install requirements ready for development
8
+ uv sync
9
+
10
+ format: ## Format the code correctly
11
+ uv run ruff format .
12
+ uv run ruff check --fix .
13
+
14
+ clean: ## Clear any cache files and test files
15
+ rm -rf .pytest_cache
16
+ rm -rf .ruff_cache
17
+ rm -rf dist/
18
+ rm -rf **/__pycache__
19
+ rm -rf **/*.pyc
20
+ rm -rf htmlcov/
21
+ rm -rf .coverage
22
+ rm -rf site/
23
+
24
+ test: ## Run tests
25
+ uv run pytest -vvv -x --cov=atpcli --cov-report=term-missing --cov-report=html --cov-config=pyproject.toml
26
+
27
+ docs-serve: ## Run a local documentation server
28
+ uv run mkdocs serve
29
+
30
+ docs-build: ## Build the documentation
31
+ uv run mkdocs build
32
+
33
+ shell: ## Run a Python shell
34
+ uv run python
35
+
36
+ release: ## Build a new version and release it
37
+ uv build && uv publish
atpcli-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: atpcli
3
+ Version: 0.1.0
4
+ Summary: A Python CLI wrapper around the atproto package
5
+ Project-URL: Homepage, https://github.com/phalt/atpcli
6
+ Project-URL: Issues, https://github.com/phalt/atpcli/issues
7
+ Author-email: Paul Hallett <paulandrewhallett@gmail.com>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: atproto>=0.0.55
12
+ Requires-Dist: click>=8.1.7
13
+ Requires-Dist: rich>=13.7.0
14
+ Description-Content-Type: text/markdown
15
+
16
+ # atpcli
17
+
18
+ A Python CLI wrapper around the [atproto](https://github.com/MarshalX/atproto) package for interacting with Bluesky.
19
+
20
+ ## Documentation
21
+
22
+ Full documentation is available at [docs/](docs/):
23
+
24
+ - [Installation Guide](docs/install.md)
25
+ - [Quick Start Guide](docs/getting-started.md) - Learn how to get app passwords and use atpcli
26
+ - [Login Command](docs/usage-login.md)
27
+ - [Timeline Command](docs/usage-timeline.md)
28
+
29
+ Or serve the docs locally:
30
+
31
+ ```bash
32
+ make docs-serve
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ ### Installation
38
+
39
+ Install globally using [uv](https://docs.astral.sh/uv/):
40
+
41
+ ```bash
42
+ uv tool install atpcli
43
+ ```
44
+
45
+ This installs `atpcli` as a global tool, making it available from anywhere in your terminal.
46
+
47
+ Or for development:
48
+
49
+ ```bash
50
+ git clone https://github.com/phalt/atpcli.git
51
+ cd atpcli
52
+ make install
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ ### Login
58
+
59
+ ⚠️ **Security Note**: Use Bluesky app passwords, not your main password! See the [Quick Start Guide](docs/getting-started.md) for instructions on creating an app password.
60
+
61
+ Login to your Bluesky account and save the session:
62
+
63
+ ```bash
64
+ atpcli bsky login
65
+ ```
66
+
67
+ You'll be prompted for your handle and password. The session will be saved to `~/.config/atpcli/config.json`.
68
+
69
+ ### View Timeline
70
+
71
+ View your timeline:
72
+
73
+ ```bash
74
+ atpcli bsky timeline
75
+ ```
76
+
77
+ Options:
78
+ - `--limit N` - Show N posts (default: 10)
79
+ - `--p N` - Show page N (default: 1)
80
+
81
+ Example:
82
+ ```bash
83
+ atpcli bsky timeline --limit 20
84
+ atpcli bsky timeline --p 2
85
+ ```
86
+
87
+ ## Development
88
+
89
+ ### Setup
90
+
91
+ ```bash
92
+ make install
93
+ ```
94
+
95
+ ### Run tests
96
+
97
+ ```bash
98
+ make test
99
+ ```
100
+
101
+ ### Build documentation
102
+
103
+ ```bash
104
+ make docs-build
105
+ ```
106
+
107
+ ### Serve documentation locally
108
+
109
+ ```bash
110
+ make docs-serve
111
+ ```
112
+
113
+ ### Format code
114
+
115
+ ```bash
116
+ make format
117
+ ```
118
+
119
+ ### Clean build artifacts
120
+
121
+ ```bash
122
+ make clean
123
+ ```
124
+
125
+ ## Requirements
126
+
127
+ - Python 3.10+
128
+ - uv package manager
129
+
130
+ ## License
131
+
132
+ MIT
atpcli-0.1.0/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # atpcli
2
+
3
+ A Python CLI wrapper around the [atproto](https://github.com/MarshalX/atproto) package for interacting with Bluesky.
4
+
5
+ ## Documentation
6
+
7
+ Full documentation is available at [docs/](docs/):
8
+
9
+ - [Installation Guide](docs/install.md)
10
+ - [Quick Start Guide](docs/getting-started.md) - Learn how to get app passwords and use atpcli
11
+ - [Login Command](docs/usage-login.md)
12
+ - [Timeline Command](docs/usage-timeline.md)
13
+
14
+ Or serve the docs locally:
15
+
16
+ ```bash
17
+ make docs-serve
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ### Installation
23
+
24
+ Install globally using [uv](https://docs.astral.sh/uv/):
25
+
26
+ ```bash
27
+ uv tool install atpcli
28
+ ```
29
+
30
+ This installs `atpcli` as a global tool, making it available from anywhere in your terminal.
31
+
32
+ Or for development:
33
+
34
+ ```bash
35
+ git clone https://github.com/phalt/atpcli.git
36
+ cd atpcli
37
+ make install
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### Login
43
+
44
+ ⚠️ **Security Note**: Use Bluesky app passwords, not your main password! See the [Quick Start Guide](docs/getting-started.md) for instructions on creating an app password.
45
+
46
+ Login to your Bluesky account and save the session:
47
+
48
+ ```bash
49
+ atpcli bsky login
50
+ ```
51
+
52
+ You'll be prompted for your handle and password. The session will be saved to `~/.config/atpcli/config.json`.
53
+
54
+ ### View Timeline
55
+
56
+ View your timeline:
57
+
58
+ ```bash
59
+ atpcli bsky timeline
60
+ ```
61
+
62
+ Options:
63
+ - `--limit N` - Show N posts (default: 10)
64
+ - `--p N` - Show page N (default: 1)
65
+
66
+ Example:
67
+ ```bash
68
+ atpcli bsky timeline --limit 20
69
+ atpcli bsky timeline --p 2
70
+ ```
71
+
72
+ ## Development
73
+
74
+ ### Setup
75
+
76
+ ```bash
77
+ make install
78
+ ```
79
+
80
+ ### Run tests
81
+
82
+ ```bash
83
+ make test
84
+ ```
85
+
86
+ ### Build documentation
87
+
88
+ ```bash
89
+ make docs-build
90
+ ```
91
+
92
+ ### Serve documentation locally
93
+
94
+ ```bash
95
+ make docs-serve
96
+ ```
97
+
98
+ ### Format code
99
+
100
+ ```bash
101
+ make format
102
+ ```
103
+
104
+ ### Clean build artifacts
105
+
106
+ ```bash
107
+ make clean
108
+ ```
109
+
110
+ ## Requirements
111
+
112
+ - Python 3.10+
113
+ - uv package manager
114
+
115
+ ## License
116
+
117
+ MIT
@@ -0,0 +1,3 @@
1
+ """atpcli - A Python CLI wrapper around the atproto package."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,135 @@
1
+ """CLI commands for atpcli."""
2
+
3
+ import textwrap
4
+
5
+ import click
6
+ from atproto import Client
7
+ from rich.console import Console
8
+
9
+ from atpcli.config import Config
10
+ from atpcli.display import display_post
11
+
12
+ console = Console()
13
+
14
+ atpcli_HEADER = r"""
15
+ _ _
16
+ __ _ _ __ ____| (_)
17
+ / _` | '_ \/ ___| | |
18
+ | (_| | |_) | (__| | |
19
+ \__,_| .__/ \___|_|_|
20
+ |_|
21
+ """.strip("\n")
22
+
23
+
24
+ @click.group()
25
+ def cli():
26
+ """atpcli - A Python CLI wrapper around the atproto package."""
27
+ pass
28
+
29
+
30
+ cli.help = textwrap.dedent(f"""\
31
+ \b
32
+ {atpcli_HEADER}
33
+
34
+ 🦋 atpcli - A Python CLI wrapper around the atproto package
35
+
36
+ 📚 GitHub: https://github.com/phalt/atpcli
37
+
38
+ """).strip("\n")
39
+
40
+
41
+ @click.group()
42
+ def bsky():
43
+ """Commands for interacting with Bluesky."""
44
+ pass
45
+
46
+
47
+ cli.add_command(bsky)
48
+
49
+
50
+ @bsky.command()
51
+ @click.option("--handle", prompt="Handle", help="Your Bluesky handle")
52
+ @click.option("--password", prompt="Password", hide_input=True, help="Your Bluesky password")
53
+ def login(handle: str, password: str):
54
+ """Login to Bluesky and save session."""
55
+ try:
56
+ client = Client()
57
+ console.print(f"[blue]Logging in as {handle}...[/blue]")
58
+ profile = client.login(handle, password)
59
+
60
+ # Get the session string from the client
61
+ session_string = client.export_session_string()
62
+
63
+ # Save the session
64
+ config = Config()
65
+ config.save_session(handle, session_string)
66
+
67
+ console.print(f"[green]✓ Successfully logged in as {profile.display_name or handle}[/green]")
68
+ console.print(f"[dim]Session saved to {config.config_file}[/dim]")
69
+ except Exception as e:
70
+ console.print(f"[red]✗ Login failed: {e}[/red]")
71
+ raise SystemExit(1)
72
+
73
+
74
+ @bsky.command()
75
+ @click.option("--limit", default=10, help="Number of posts to show")
76
+ @click.option("--p", "page", default=1, help="Page number to load")
77
+ def timeline(limit: int, page: int):
78
+ """View your timeline."""
79
+ config = Config()
80
+ handle, session_string = config.load_session()
81
+
82
+ if not session_string:
83
+ console.print("[red]✗ Not logged in. Please run 'atpcli bsky login' first.[/red]")
84
+ raise SystemExit(1)
85
+
86
+ try:
87
+ client = Client()
88
+ console.print(f"[blue]Loading timeline for {handle}...[/blue]")
89
+
90
+ # Restore session from saved string
91
+ client.login(session_string=session_string)
92
+
93
+ # Calculate cursor position for pagination
94
+ # Note: We need to fetch pages sequentially to get the cursor for each page.
95
+ # This means accessing page N requires N API calls, which can be slow for high page numbers.
96
+ cursor = None
97
+ if page > 5:
98
+ warning_msg = f"[yellow]⚠ Loading page {page} requires {page} API calls. This may take a moment...[/yellow]"
99
+ console.print(warning_msg)
100
+
101
+ for i in range(1, page):
102
+ response = client.get_timeline(limit=limit, cursor=cursor)
103
+ cursor = response.cursor
104
+ if not cursor:
105
+ console.print(f"[yellow]⚠ Page {page} does not exist. Showing last available page (page {i}).[/yellow]")
106
+ page = i
107
+ break
108
+
109
+ # Get the requested page
110
+ timeline_response = client.get_timeline(limit=limit, cursor=cursor)
111
+
112
+ # Reverse the feed so latest posts appear at the bottom
113
+ # This allows users to scroll up to read
114
+ reversed_feed = list(reversed(timeline_response.feed))
115
+
116
+ for feed_view in reversed_feed:
117
+ post = feed_view.post
118
+ table = display_post(post)
119
+ console.print(table)
120
+
121
+ # Show pagination info
122
+ page_info = f"[dim]Showing {len(timeline_response.feed)} posts (page {page})"
123
+ if timeline_response.cursor:
124
+ page_info += f" - Use --p {page + 1} for next page"
125
+ page_info += "[/dim]"
126
+ console.print(f"\n{page_info}")
127
+
128
+ except Exception as e:
129
+ console.print(f"[red]✗ Failed to load timeline: {e}[/red]")
130
+ console.print("[yellow]Your session may have expired. Try logging in again.[/yellow]")
131
+ raise SystemExit(1)
132
+
133
+
134
+ if __name__ == "__main__":
135
+ cli()
@@ -0,0 +1,42 @@
1
+ """Configuration management for atpcli."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+
8
+ class Config:
9
+ """Manage atpcli configuration and session state."""
10
+
11
+ def __init__(self, config_dir: Optional[Path] = None):
12
+ """Initialize config with optional custom directory."""
13
+ if config_dir is None:
14
+ config_dir = Path.home() / ".config" / "atpcli"
15
+ self.config_dir = config_dir
16
+ self.config_file = self.config_dir / "config.json"
17
+ self.config_dir.mkdir(parents=True, exist_ok=True)
18
+
19
+ def save_session(self, handle: str, session_string: str) -> None:
20
+ """Save session information to config file."""
21
+ config_data = self.load_config()
22
+ config_data["handle"] = handle
23
+ config_data["session"] = session_string
24
+ with open(self.config_file, "w", encoding="utf-8") as f:
25
+ json.dump(config_data, f, indent=2)
26
+
27
+ def load_session(self) -> tuple[Optional[str], Optional[str]]:
28
+ """Load session information from config file."""
29
+ config_data = self.load_config()
30
+ return config_data.get("handle"), config_data.get("session")
31
+
32
+ def load_config(self) -> dict:
33
+ """Load the entire config file."""
34
+ if not self.config_file.exists():
35
+ return {}
36
+ with open(self.config_file, "r", encoding="utf-8") as f:
37
+ return json.load(f)
38
+
39
+ def clear_session(self) -> None:
40
+ """Clear the saved session."""
41
+ if self.config_file.exists():
42
+ self.config_file.unlink()