bmv-cli 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.
bmv_cli-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: bmv-cli
3
+ Version: 0.1.0
4
+ Summary: CLI tool for Mexican stock market (BMV/BIVA) data via DataBursatil API
5
+ Author: Axel Berrelleza
6
+ License-Expression: MIT
7
+ Classifier: Environment :: Console
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Intended Audience :: Financial and Insurance Industry
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Topic :: Office/Business :: Financial :: Investment
14
+ Classifier: Topic :: Terminals
15
+ Requires-Dist: python-dotenv>=1.2.1
16
+ Requires-Dist: requests>=2.32.5
17
+ Requires-Dist: rich>=14.2.0
18
+ Requires-Dist: typer>=0.20.0
19
+ Requires-Dist: pytest>=8.3.5 ; extra == 'dev'
20
+ Requires-Dist: requests-mock>=1.12.1 ; extra == 'dev'
21
+ Requires-Dist: ruff>=0.9.0 ; extra == 'dev'
22
+ Requires-Dist: mypy>=1.15.0 ; extra == 'dev'
23
+ Requires-Dist: types-requests>=2.32.0.20241016 ; extra == 'dev'
24
+ Requires-Python: >=3.12
25
+ Project-URL: Homepage, https://github.com/AxelBerrelleza/bmv-cli
26
+ Project-URL: Issues, https://github.com/AxelBerrelleza/bmv-cli/issues
27
+ Provides-Extra: dev
28
+ Description-Content-Type: text/markdown
29
+
30
+ # bmv-cli
31
+
32
+ A command-line interface for Mexican stock market (BMV/BIVA) data via the [DataBursatil](https://www.databursatil.com) API v2.
33
+
34
+ Get real-time quotes, currency rates, API credits, search for symbols, and dividend information — all from your terminal.
35
+
36
+ ## Installation
37
+
38
+ ### uv tool (recommended)
39
+ ```bash
40
+ uv tool install bmv-cli
41
+ ```
42
+
43
+ ### pipx
44
+ ```bash
45
+ pipx install bmv-cli
46
+ ```
47
+
48
+ ### pip
49
+ ```bash
50
+ pip install bmv-cli
51
+ ```
52
+
53
+ ## Setup
54
+
55
+ You need a DataBursatil API token. Get one at <https://www.databursatil.com>.
56
+
57
+ Set the token as an environment variable:
58
+
59
+ ```bash
60
+ export BMV_API_TOKEN="your-token-here"
61
+ ```
62
+
63
+ For local development, you can also create a `.env` file in the project root:
64
+
65
+ ```bash
66
+ cp .env.sample .env
67
+ # Edit .env and add your BMV_API_TOKEN
68
+ ```
69
+
70
+ ## Usage
71
+
72
+ All commands require the `BMV_API_TOKEN` environment variable.
73
+
74
+ ### Get real-time prices
75
+ ```bash
76
+ bmv price GFNORTEO WALMEX*
77
+ ```
78
+
79
+ ### Get currency exchange rates
80
+ ```bash
81
+ bmv currencies
82
+ ```
83
+
84
+ ### Check remaining API credits
85
+ ```bash
86
+ bmv credits
87
+ ```
88
+
89
+ ### Search for stock symbols
90
+ ```bash
91
+ bmv search WALMEX
92
+ bmv search GFNORTE --market global
93
+ ```
94
+
95
+ ### Get dividend information
96
+ ```bash
97
+ bmv dividend FMTY14
98
+ bmv dividend WALMEX*
99
+ ```
100
+
101
+ ## Development
102
+
103
+ ```bash
104
+ # Clone and setup
105
+ git clone https://github.com/AxelBerrelleza/bmv-cli.git
106
+ cd bmv-cli
107
+ uv sync --extra dev
108
+
109
+ # Run quality gates
110
+ uv run ruff format --check .
111
+ uv run ruff check .
112
+ uv run mypy src/bmv_cli/api src/bmv_cli/commands
113
+ uv run pytest
114
+
115
+ # Run the CLI during development
116
+ uv run bmv --help
117
+ ```
118
+
119
+ See [AGENTS.md](AGENTS.md) for detailed development instructions.
@@ -0,0 +1,90 @@
1
+ # bmv-cli
2
+
3
+ A command-line interface for Mexican stock market (BMV/BIVA) data via the [DataBursatil](https://www.databursatil.com) API v2.
4
+
5
+ Get real-time quotes, currency rates, API credits, search for symbols, and dividend information — all from your terminal.
6
+
7
+ ## Installation
8
+
9
+ ### uv tool (recommended)
10
+ ```bash
11
+ uv tool install bmv-cli
12
+ ```
13
+
14
+ ### pipx
15
+ ```bash
16
+ pipx install bmv-cli
17
+ ```
18
+
19
+ ### pip
20
+ ```bash
21
+ pip install bmv-cli
22
+ ```
23
+
24
+ ## Setup
25
+
26
+ You need a DataBursatil API token. Get one at <https://www.databursatil.com>.
27
+
28
+ Set the token as an environment variable:
29
+
30
+ ```bash
31
+ export BMV_API_TOKEN="your-token-here"
32
+ ```
33
+
34
+ For local development, you can also create a `.env` file in the project root:
35
+
36
+ ```bash
37
+ cp .env.sample .env
38
+ # Edit .env and add your BMV_API_TOKEN
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ All commands require the `BMV_API_TOKEN` environment variable.
44
+
45
+ ### Get real-time prices
46
+ ```bash
47
+ bmv price GFNORTEO WALMEX*
48
+ ```
49
+
50
+ ### Get currency exchange rates
51
+ ```bash
52
+ bmv currencies
53
+ ```
54
+
55
+ ### Check remaining API credits
56
+ ```bash
57
+ bmv credits
58
+ ```
59
+
60
+ ### Search for stock symbols
61
+ ```bash
62
+ bmv search WALMEX
63
+ bmv search GFNORTE --market global
64
+ ```
65
+
66
+ ### Get dividend information
67
+ ```bash
68
+ bmv dividend FMTY14
69
+ bmv dividend WALMEX*
70
+ ```
71
+
72
+ ## Development
73
+
74
+ ```bash
75
+ # Clone and setup
76
+ git clone https://github.com/AxelBerrelleza/bmv-cli.git
77
+ cd bmv-cli
78
+ uv sync --extra dev
79
+
80
+ # Run quality gates
81
+ uv run ruff format --check .
82
+ uv run ruff check .
83
+ uv run mypy src/bmv_cli/api src/bmv_cli/commands
84
+ uv run pytest
85
+
86
+ # Run the CLI during development
87
+ uv run bmv --help
88
+ ```
89
+
90
+ See [AGENTS.md](AGENTS.md) for detailed development instructions.
@@ -0,0 +1,109 @@
1
+ [project]
2
+ name = "bmv-cli"
3
+ version = "0.1.0"
4
+ description = "CLI tool for Mexican stock market (BMV/BIVA) data via DataBursatil API"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ classifiers = [
9
+ "Environment :: Console",
10
+ "Intended Audience :: Developers",
11
+ "Intended Audience :: Financial and Insurance Industry",
12
+ "Programming Language :: Python :: 3.12",
13
+ "Programming Language :: Python :: 3.13",
14
+ "Programming Language :: Python :: 3.14",
15
+ "Topic :: Office/Business :: Financial :: Investment",
16
+ "Topic :: Terminals",
17
+ ]
18
+ dependencies = [
19
+ "python-dotenv>=1.2.1",
20
+ "requests>=2.32.5",
21
+ "rich>=14.2.0",
22
+ "typer>=0.20.0",
23
+ ]
24
+
25
+ [[project.authors]]
26
+ name = "Axel Berrelleza"
27
+
28
+ [project.scripts]
29
+ bmv = "bmv_cli.main:app"
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/AxelBerrelleza/bmv-cli"
33
+ Issues = "https://github.com/AxelBerrelleza/bmv-cli/issues"
34
+
35
+ [project.optional-dependencies]
36
+ dev = [
37
+ "pytest>=8.3.5",
38
+ "requests-mock>=1.12.1",
39
+ "ruff>=0.9.0",
40
+ "mypy>=1.15.0",
41
+ "types-requests>=2.32.0.20241016",
42
+ ]
43
+
44
+ [build-system]
45
+ requires = ["uv_build>=0.13.6,<0.14"]
46
+ build-backend = "uv_build"
47
+
48
+ [tool.ruff]
49
+ target-version = "py312"
50
+ line-length = 100
51
+ exclude = [
52
+ ".venv",
53
+ ".git",
54
+ "__pycache__",
55
+ "build",
56
+ "dist",
57
+ ]
58
+
59
+ [tool.ruff.lint]
60
+ select = [
61
+ "E",
62
+ "W",
63
+ "F",
64
+ "I",
65
+ "UP",
66
+ "B",
67
+ "SIM",
68
+ "RUF",
69
+ "TCH",
70
+ ]
71
+ ignore = [
72
+ "E501",
73
+ "B008",
74
+ "B904",
75
+ "B905",
76
+ "RUF012",
77
+ "SIM113",
78
+ "SIM117",
79
+ ]
80
+
81
+ [tool.ruff.lint.isort]
82
+ known-first-party = ["bmv_cli"]
83
+
84
+ [tool.ruff.format]
85
+ quote-style = "double"
86
+ indent-style = "space"
87
+ skip-magic-trailing-comma = false
88
+ line-ending = "auto"
89
+
90
+ [tool.mypy]
91
+ python_version = "3.12"
92
+ warn_return_any = false
93
+ warn_unused_configs = true
94
+ warn_unused_ignores = true
95
+ check_untyped_defs = true
96
+ disallow_untyped_decorators = false
97
+ no_implicit_optional = true
98
+ show_error_codes = true
99
+
100
+ [[tool.mypy.overrides]]
101
+ module = "bmv_cli.commands.tests.*"
102
+ ignore_errors = true
103
+
104
+ [tool.pytest.ini_options]
105
+ minversion = "8.0"
106
+ testpaths = ["src/bmv_cli/commands/tests"]
107
+ log_cli = true
108
+ log_cli_level = "INFO"
109
+ addopts = "-v --tb=short"
@@ -0,0 +1,118 @@
1
+ [project]
2
+ name = "bmv-cli"
3
+ version = "0.1.0"
4
+ description = "CLI tool for Mexican stock market (BMV/BIVA) data via DataBursatil API"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ authors = [
9
+ { name = "Axel Berrelleza" }
10
+ ]
11
+ classifiers = [
12
+ "Environment :: Console",
13
+ "Intended Audience :: Developers",
14
+ "Intended Audience :: Financial and Insurance Industry",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Programming Language :: Python :: 3.13",
17
+ "Programming Language :: Python :: 3.14",
18
+ "Topic :: Office/Business :: Financial :: Investment",
19
+ "Topic :: Terminals",
20
+ ]
21
+ dependencies = [
22
+ "python-dotenv>=1.2.1",
23
+ "requests>=2.32.5",
24
+ "rich>=14.2.0",
25
+ "typer>=0.20.0",
26
+ ]
27
+
28
+ [project.scripts]
29
+ bmv = "bmv_cli.main:app"
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/AxelBerrelleza/bmv-cli"
33
+ Issues = "https://github.com/AxelBerrelleza/bmv-cli/issues"
34
+
35
+ [project.optional-dependencies]
36
+ dev = [
37
+ "pytest>=8.3.5",
38
+ "requests-mock>=1.12.1",
39
+ "ruff>=0.9.0",
40
+ "mypy>=1.15.0",
41
+ "types-requests>=2.32.0.20241016",
42
+ ]
43
+
44
+ [build-system]
45
+ requires = ["uv_build>=0.13.6,<0.14"]
46
+ build-backend = "uv_build"
47
+
48
+ # ==============================================================================
49
+ # Ruff Configuration (Formatting & Linting)
50
+ # ==============================================================================
51
+ [tool.ruff]
52
+ target-version = "py312"
53
+ line-length = 100
54
+ exclude = [
55
+ ".venv",
56
+ ".git",
57
+ "__pycache__",
58
+ "build",
59
+ "dist",
60
+ ]
61
+
62
+ [tool.ruff.lint]
63
+ select = [
64
+ "E", # pycodestyle errors
65
+ "W", # pycodestyle warnings
66
+ "F", # Pyflakes
67
+ "I", # isort (import sorting)
68
+ "UP", # pyupgrade (modern Python 3.12+ syntax)
69
+ "B", # flake8-bugbear
70
+ "SIM", # flake8-simplify
71
+ "RUF", # Ruff-specific rules
72
+ "TCH", # flake8-type-checking
73
+ ]
74
+ ignore = [
75
+ "E501", # Line too long (handled by ruff format)
76
+ "B008", # Do not perform function calls in argument defaults (needed for Typer)
77
+ "B904", # Within an except clause, raise exceptions with from err
78
+ "B905", # zip() without an explicit strict= parameter
79
+ "RUF012", # Mutable default value for class attribute
80
+ "SIM113", # Use enumerate() for index variable in for loop
81
+ "SIM117", # Combine nested with statements
82
+ ]
83
+
84
+ [tool.ruff.lint.isort]
85
+ known-first-party = ["bmv_cli"]
86
+
87
+ [tool.ruff.format]
88
+ quote-style = "double"
89
+ indent-style = "space"
90
+ skip-magic-trailing-comma = false
91
+ line-ending = "auto"
92
+
93
+ # ==============================================================================
94
+ # Mypy Configuration (Static Analysis & Type Checking)
95
+ # ==============================================================================
96
+ [tool.mypy]
97
+ python_version = "3.12"
98
+ warn_return_any = false
99
+ warn_unused_configs = true
100
+ warn_unused_ignores = true
101
+ check_untyped_defs = true
102
+ disallow_untyped_decorators = false
103
+ no_implicit_optional = true
104
+ show_error_codes = true
105
+
106
+ [[tool.mypy.overrides]]
107
+ module = "bmv_cli.commands.tests.*"
108
+ ignore_errors = true
109
+
110
+ # ==============================================================================
111
+ # Pytest Configuration
112
+ # ==============================================================================
113
+ [tool.pytest.ini_options]
114
+ minversion = "8.0"
115
+ testpaths = ["src/bmv_cli/commands/tests"]
116
+ log_cli = true
117
+ log_cli_level = "INFO"
118
+ addopts = "-v --tb=short"
@@ -0,0 +1 @@
1
+ """BMV CLI - Mexican stock market data via DataBursatil API."""
File without changes
@@ -0,0 +1,39 @@
1
+ from os import getenv
2
+
3
+ import requests
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+
8
+
9
+ def get_base_url(endpoint: str, withExchange: bool) -> str:
10
+ return f"https://api.databursatil.com/v2/{endpoint}?token={getenv('BMV_API_TOKEN')}" + (
11
+ "&bolsa=bmv,biva" if withExchange else ""
12
+ )
13
+
14
+
15
+ def request_prices(symbols: list[str]):
16
+ response = requests.get(
17
+ get_base_url("cotizaciones", withExchange=True)
18
+ + "&concepto=u,p,a,x,n,c,m,v,o,i"
19
+ + "&emisora_serie="
20
+ + ",".join(symbols)
21
+ )
22
+ return response.json()
23
+
24
+
25
+ def request_currencies():
26
+ response = requests.get(get_base_url("divisas", withExchange=False))
27
+ return response.json()
28
+
29
+
30
+ def request_credits():
31
+ response = requests.get(get_base_url("creditos", withExchange=False))
32
+ return response.json()
33
+
34
+
35
+ def request_emisoras(letra: str, mercado: str = "local"):
36
+ response = requests.get(
37
+ get_base_url("emisoras", withExchange=False) + f"&letra={letra}&mercado={mercado}"
38
+ )
39
+ return response.json()
File without changes
@@ -0,0 +1,14 @@
1
+ import typer
2
+ from rich.console import Console
3
+
4
+ from bmv_cli.api.client import request_credits
5
+
6
+ app = typer.Typer()
7
+
8
+
9
+ @app.command()
10
+ def credits():
11
+ """Check remaining API credits"""
12
+ response = request_credits()
13
+ console = Console()
14
+ console.print(f"[bold green]Remaining credits:[/] {response['disponibles']}")
@@ -0,0 +1,27 @@
1
+ import typer
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+
5
+ from bmv_cli.api.client import request_currencies
6
+
7
+ app = typer.Typer()
8
+
9
+
10
+ @app.command()
11
+ def currencies():
12
+ response = request_currencies()
13
+ table = Table(
14
+ "Currency",
15
+ "Last Value",
16
+ "Change %",
17
+ "Change",
18
+ )
19
+ date = response.get("t")
20
+ response.pop("t")
21
+ for currency, data in response.items():
22
+ # pp(data)
23
+ table.add_row(currency, *[str(value) for value in data.values()])
24
+
25
+ console = Console()
26
+ console.print(table)
27
+ console.print(f"Datetime: {date}")
@@ -0,0 +1,102 @@
1
+ import re
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+
7
+ from bmv_cli.api.client import request_emisoras
8
+
9
+ app = typer.Typer()
10
+
11
+
12
+ def extract_emitter_name(symbol: str) -> str:
13
+ """Extract emitter name from symbol+series (e.g., FMTY14 -> FMTY, GFNORTEO -> GFNORTE, WALMEX* -> WALMEX)"""
14
+ symbol = symbol.upper()
15
+
16
+ # If ends with *, strip it
17
+ if symbol.endswith("*"):
18
+ return symbol[:-1]
19
+
20
+ # If ends with digits, strip them
21
+ match = re.match(r"^([A-Z]+)\d+$", symbol)
22
+ if match:
23
+ return match.group(1)
24
+
25
+ # If ends with a single letter, try stripping it (risky but common pattern)
26
+ # Only do this if the remaining part is at least 3 characters
27
+ if len(symbol) > 3 and symbol[-1].isalpha():
28
+ return symbol[:-1]
29
+
30
+ return symbol
31
+
32
+
33
+ @app.command()
34
+ def dividend(
35
+ symbol: str = typer.Argument(..., help="Stock symbol (e.g., FMTY, FMTY14, WALMEX*)"),
36
+ ):
37
+ """Get dividend information for a symbol"""
38
+ console = Console()
39
+
40
+ # Try the symbol as-is first
41
+ response = request_emisoras(symbol)
42
+
43
+ # If API returns an error, try extracting just the emitter name
44
+ if "Error" in response:
45
+ emitter_name = extract_emitter_name(symbol)
46
+ if emitter_name != symbol:
47
+ console.print(f"[yellow]Trying with emitter name: {emitter_name}[/yellow]")
48
+ response = request_emisoras(emitter_name)
49
+
50
+ # Still an error?
51
+ if "Error" in response:
52
+ console.print(f"[red]Error: {response['Error']}[/red]")
53
+ raise typer.Exit(code=1)
54
+
55
+ # Response structure: {SYMBOL: {SERIE: {data...}}}
56
+ found = False
57
+ for emitter_key, series_data in response.items():
58
+ if not isinstance(series_data, dict):
59
+ continue
60
+
61
+ for serie, data in series_data.items():
62
+ if not isinstance(data, dict):
63
+ continue
64
+
65
+ dividendos = data.get("dividendos", {})
66
+ found = True
67
+
68
+ if not dividendos:
69
+ console.print(
70
+ f"[yellow]No dividend information available for {emitter_key}{serie}[/yellow]"
71
+ )
72
+ continue
73
+
74
+ console.print(f"\n[bold]Dividend Info for {emitter_key}{serie}:[/]")
75
+
76
+ # Recent dividend
77
+ if "reciente" in dividendos:
78
+ console.print("\n[bold green]Most Recent Dividend(s):[/]")
79
+ for date, info in dividendos["reciente"].items():
80
+ console.print(f" Date: {date}")
81
+ console.print(f" Ex-coupon Date: {info.get('fechaexcupon', 'N/A')}")
82
+ console.print(f" Amount: {info.get('pago', 'N/A')} {info.get('divisa', '')}")
83
+ console.print(f" Type: {info.get('tipo', 'N/A')}")
84
+ console.print()
85
+
86
+ # Historical dividends
87
+ if "historico" in dividendos:
88
+ console.print("\n[bold blue]Dividend History:[/]")
89
+ table = Table("Date", "Amount", "Type")
90
+
91
+ for date, info in dividendos["historico"].items():
92
+ table.add_row(
93
+ date,
94
+ str(info.get("pago", "N/A")),
95
+ info.get("tipo", "N/A"),
96
+ )
97
+
98
+ console.print(table)
99
+
100
+ if not found:
101
+ console.print(f"[red]No data found for symbol: {symbol}[/red]")
102
+ raise typer.Exit(code=1)
@@ -0,0 +1,8 @@
1
+ import typer
2
+
3
+ app = typer.Typer()
4
+
5
+
6
+ @app.command()
7
+ def hello():
8
+ print("Hello world")
@@ -0,0 +1,32 @@
1
+ import typer
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+
5
+ from bmv_cli.api.client import request_prices
6
+
7
+ app = typer.Typer()
8
+
9
+
10
+ @app.command()
11
+ def price(symbols: list[str]):
12
+ response = request_prices(symbols)
13
+ table = Table(
14
+ "simbolo",
15
+ "ultimo",
16
+ "ppp",
17
+ "anterior",
18
+ "maximo",
19
+ "minimo",
20
+ "cambioporciento",
21
+ "cambiopesos",
22
+ "volumenoperado",
23
+ "operaciones",
24
+ "importeacumulado",
25
+ "fecha",
26
+ )
27
+ for symbol, data in response.items():
28
+ row = data["bmv"] if data["bmv"]["f"] > data["biva"]["f"] else data["biva"]
29
+ table.add_row(symbol, *[str(value) for value in row.values()])
30
+
31
+ console = Console()
32
+ console.print(table)
@@ -0,0 +1,49 @@
1
+ import typer
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+
5
+ from bmv_cli.api.client import request_emisoras
6
+
7
+ app = typer.Typer()
8
+
9
+
10
+ @app.command()
11
+ def search(
12
+ text: str = typer.Argument(..., help="Search text (letter or company name)"),
13
+ market: str = typer.Option(
14
+ "local",
15
+ "--market",
16
+ "-m",
17
+ help="Market type: local or global",
18
+ case_sensitive=False,
19
+ ),
20
+ ):
21
+ """Search for stock symbols"""
22
+ response = request_emisoras(text, market)
23
+ table = Table(
24
+ "Symbol",
25
+ "Status",
26
+ "Company Name",
27
+ "Exchange",
28
+ "Type",
29
+ "Shares Outstanding",
30
+ "ISIN",
31
+ )
32
+
33
+ console = Console()
34
+
35
+ # Response structure: {SYMBOL: {SERIE: {data...}}}
36
+ for symbol, series_data in response.items():
37
+ for serie, data in series_data.items():
38
+ if isinstance(data, dict):
39
+ table.add_row(
40
+ f"{symbol}{serie}",
41
+ data.get("estatus", "N/A"),
42
+ data.get("razon_social", "N/A"),
43
+ data.get("bolsa", "N/A"),
44
+ data.get("tipo_valor_descripcion", "N/A"),
45
+ str(data.get("acciones_en_circulacion", "N/A")),
46
+ data.get("isin", "N/A"),
47
+ )
48
+
49
+ console.print(table)
File without changes
@@ -0,0 +1,9 @@
1
+ import pytest
2
+
3
+
4
+ @pytest.fixture
5
+ def cli_runner():
6
+ """Provide a Typer CliRunner for testing CLI commands."""
7
+ from typer.testing import CliRunner
8
+
9
+ return CliRunner()
@@ -0,0 +1,134 @@
1
+ """Test fixtures for API responses - DataBursatil v2 API."""
2
+
3
+
4
+ def prices_response():
5
+ """Mock response from /v2/cotizaciones endpoint."""
6
+ return {
7
+ "GFNORTEO": {
8
+ "bmv": {
9
+ "u": 213.5,
10
+ "p": 213.46,
11
+ "a": 207.91,
12
+ "x": 214.69,
13
+ "n": 206.89,
14
+ "c": 2.67,
15
+ "m": 5.55,
16
+ "v": 8081217.0,
17
+ "o": 25245.0,
18
+ "i": 1710147325.48,
19
+ "f": "2026-10-23 14:00:00",
20
+ },
21
+ "biva": {
22
+ "u": 213.5,
23
+ "p": 213.46,
24
+ "a": 207.91,
25
+ "x": 214.43,
26
+ "n": 207.49,
27
+ "c": 2.68,
28
+ "m": 5.59,
29
+ "v": 1190223.0,
30
+ "o": 6307.0,
31
+ "i": 252457560.01,
32
+ "f": "2026-10-23 14:05:00",
33
+ },
34
+ },
35
+ "WALMEX*": {
36
+ "bmv": {
37
+ "u": 50.73,
38
+ "p": 50.77,
39
+ "a": 50.34,
40
+ "x": 50.94,
41
+ "n": 50.01,
42
+ "c": 0.85,
43
+ "m": 0.43,
44
+ "v": 8904568.0,
45
+ "o": 18587.0,
46
+ "i": 451685201.71,
47
+ "f": "2026-10-23 14:00:00",
48
+ },
49
+ "biva": {
50
+ "u": 50.88,
51
+ "p": 50.77,
52
+ "a": 50.34,
53
+ "x": 50.94,
54
+ "n": 50.12,
55
+ "c": 1.07,
56
+ "m": 0.53,
57
+ "v": 1714520.0,
58
+ "o": 6643.0,
59
+ "i": 86940490.91,
60
+ "f": "2026-10-23 14:05:00",
61
+ },
62
+ },
63
+ }
64
+
65
+
66
+ def currencies_response():
67
+ """Mock response from /v2/divisas endpoint."""
68
+ return {
69
+ "AUDUSD": {"u": 0.6903, "c": -0.1, "m": -0.0007},
70
+ "CADMXN": {"u": 12.3493, "c": 0.16, "m": 0.0196},
71
+ "CHFMXN": {"u": 21.6778, "c": 0.3, "m": 0.0659},
72
+ "EURMXN": {"u": 19.9913, "c": 0.42, "m": 0.0833},
73
+ "EURUSD": {"u": 1.1408, "c": 0.34, "m": 0.0039},
74
+ "GBPMXN": {"u": 23.1671, "c": 0.29, "m": 0.0671},
75
+ "GBPUSD": {"u": 1.3224, "c": 0.24, "m": 0.0031},
76
+ "USDCAD": {"u": 1.4181, "c": -0.14, "m": -0.002},
77
+ "USDJPY": {"u": 161.55, "c": -0.15, "m": -0.24},
78
+ "USDMXN": {"u": 17.5147, "c": 0.03, "m": 0.0047},
79
+ "t": "2026-06-26 04:01:00",
80
+ }
81
+
82
+
83
+ def credits_response():
84
+ """Mock response from /v2/creditos endpoint."""
85
+ return {"disponibles": 249421}
86
+
87
+
88
+ def emisoras_response():
89
+ """Mock response from /v2/emisoras endpoint."""
90
+ return {
91
+ "WALMEX": {
92
+ "*": {
93
+ "razon_social": "WAL - MART DE MEXICO, S.A.B. DE C.V.",
94
+ "isin": "MX01WA000038",
95
+ "bolsa": "BMV",
96
+ "tipo_valor_descripcion": "ACCIONES DE EMPRESAS COMERCIALES, INDUSTRIALES Y DE SERVICIOS",
97
+ "tipo_valor_id": "1",
98
+ "estatus": "ACTIVA",
99
+ "acciones_en_circulacion": 17461402631,
100
+ "rango_historicos": "2010-01-05 a 2026-10-23",
101
+ "rango_financieros": "1T_2017, 1T_2018, 1T_2019, 1T_2020, 1T_2021, 1T_2022, 1T_2023, 1T_2024, 1T_2025, 1T_2026",
102
+ "dividendos": {
103
+ "reciente": {
104
+ "2025-12-17": {
105
+ "pago": 0.85,
106
+ "tipo": "DIVIDENDO EN EFECTIVO",
107
+ "divisa": "MXN",
108
+ "fechaexcupon": "2025-12-16",
109
+ }
110
+ },
111
+ "historico": {
112
+ "2021-11-24": {"pago": 0.81, "tipo": "DIVIDENDO EN EFECTIVO"},
113
+ "2021-12-28": {"pago": 0.82, "tipo": "DIVIDENDO EN EFECTIVO"},
114
+ "2022-11-22": {"pago": 0.85, "tipo": "DIVIDENDO EN EFECTIVO"},
115
+ "2022-12-27": {"pago": 0.86, "tipo": "DIVIDENDO EN EFECTIVO"},
116
+ "2023-04-11": {"pago": 0.75, "tipo": "DIVIDENDO EN EFECTIVO"},
117
+ "2023-11-29": {"pago": 0.97, "tipo": "DIVIDENDO EN EFECTIVO"},
118
+ "2023-12-06": {"pago": 0.97, "tipo": "DIVIDENDO EN EFECTIVO"},
119
+ "2024-11-12": {"pago": 0.59, "tipo": "DIVIDENDO EN EFECTIVO"},
120
+ "2024-12-10": {"pago": 1.09, "tipo": "DIVIDENDO EN EFECTIVO"},
121
+ "2025-11-19": {"pago": 0.84, "tipo": "DIVIDENDO EN EFECTIVO"},
122
+ "2025-12-17": {"pago": 0.85, "tipo": "DIVIDENDO EN EFECTIVO"},
123
+ },
124
+ },
125
+ }
126
+ }
127
+ }
128
+
129
+
130
+ def emisoras_error_response():
131
+ """Mock error response from /v2/emisoras endpoint."""
132
+ return {
133
+ "Error": "La busqueda no arrojo ningun resultado. ¿Dudas? Revisa la documentacion: https://databursatil.com/docs.html"
134
+ }
@@ -0,0 +1,17 @@
1
+ """Test fixtures package."""
2
+
3
+ from .APIResponses import (
4
+ credits_response,
5
+ currencies_response,
6
+ emisoras_error_response,
7
+ emisoras_response,
8
+ prices_response,
9
+ )
10
+
11
+ __all__ = [
12
+ "credits_response",
13
+ "currencies_response",
14
+ "emisoras_error_response",
15
+ "emisoras_response",
16
+ "prices_response",
17
+ ]
@@ -0,0 +1,51 @@
1
+ import pytest
2
+ from typer.testing import CliRunner
3
+
4
+ from bmv_cli.commands.currencies import app
5
+ from bmv_cli.commands.tests.fixtures import currencies_response
6
+
7
+ runner = CliRunner()
8
+
9
+
10
+ @pytest.fixture
11
+ def mock_currencies_api(requests_mock):
12
+ """Mock the divisas API endpoint."""
13
+ response = currencies_response()
14
+ requests_mock.get(
15
+ "https://api.databursatil.com/v2/divisas",
16
+ json=response,
17
+ )
18
+ return response
19
+
20
+
21
+ def test_currencies_command(requests_mock, mock_currencies_api):
22
+ """Test currencies command with mocked API."""
23
+ result = runner.invoke(app, [], env={"COLUMNS": "200"})
24
+
25
+ assert result.exit_code == 0
26
+ # Check that some currencies appear in output
27
+ assert "USDMXN" in result.stdout
28
+ assert "EURUSD" in result.stdout
29
+
30
+
31
+ def test_currencies_table_data(requests_mock, mock_currencies_api):
32
+ """Verify table output contains expected currency data."""
33
+ result = runner.invoke(app, [], env={"COLUMNS": "200"})
34
+
35
+ assert result.exit_code == 0
36
+ # Check for table headers
37
+ assert "Currency" in result.stdout
38
+ assert "Last Value" in result.stdout
39
+ # Check for specific currency values from the fixture
40
+ assert "17.5147" in result.stdout # USDMXN rate
41
+ assert "1.1408" in result.stdout # EURUSD rate
42
+
43
+
44
+ def test_currencies_date_display(requests_mock, mock_currencies_api):
45
+ """Verify date display."""
46
+ result = runner.invoke(app, [], env={"COLUMNS": "200"})
47
+
48
+ assert result.exit_code == 0
49
+ # Check that the datetime is displayed
50
+ assert "Datetime:" in result.stdout
51
+ assert "2026-06-26 04:01:00" in result.stdout
@@ -0,0 +1,12 @@
1
+ from typer.testing import CliRunner
2
+
3
+ from bmv_cli.commands.hello import app
4
+
5
+
6
+ def test_hello_command():
7
+ """Test hello command output and exit code."""
8
+ runner = CliRunner()
9
+ result = runner.invoke(app, [])
10
+
11
+ assert result.exit_code == 0
12
+ assert "Hello world" in result.stdout
@@ -0,0 +1,100 @@
1
+ import pytest
2
+ from typer.testing import CliRunner
3
+
4
+ from bmv_cli.commands.price import app
5
+ from bmv_cli.commands.tests.fixtures import prices_response
6
+
7
+ runner = CliRunner()
8
+
9
+
10
+ @pytest.fixture
11
+ def mock_prices_api(requests_mock):
12
+ """Mock the cotizaciones API endpoint."""
13
+ response = prices_response()
14
+ requests_mock.get(
15
+ "https://api.databursatil.com/v2/cotizaciones",
16
+ json=response,
17
+ )
18
+ return response
19
+
20
+
21
+ def test_single_symbol_price(requests_mock, mock_prices_api):
22
+ """Test single symbol price lookup with mocked API."""
23
+ result = runner.invoke(app, ["GFNORTEO"], env={"COLUMNS": "200"})
24
+
25
+ assert result.exit_code == 0
26
+ assert "GFNORTEO" in result.stdout
27
+ # Should show BIVA data (more recent timestamp) - ultimo price 213.5
28
+ assert "213.5" in result.stdout
29
+
30
+
31
+ def test_multiple_symbols_price(requests_mock, mock_prices_api):
32
+ """Test multiple symbols price lookup."""
33
+ result = runner.invoke(app, ["GFNORTEO", "WALMEX*"], env={"COLUMNS": "200"})
34
+
35
+ assert result.exit_code == 0
36
+ assert "GFNORTEO" in result.stdout
37
+ assert "WALMEX*" in result.stdout
38
+
39
+
40
+ def test_price_table_columns(requests_mock, mock_prices_api):
41
+ """Verify table output contains expected columns and data."""
42
+ result = runner.invoke(app, ["GFNORTEO"], env={"COLUMNS": "200"})
43
+
44
+ assert result.exit_code == 0
45
+ # Check for table headers (full names with wide terminal)
46
+ assert "simbolo" in result.stdout
47
+ assert "ultimo" in result.stdout
48
+ assert "fecha" in result.stdout
49
+ # Check for data values from the BIVA response (more recent)
50
+ assert "213.5" in result.stdout
51
+ assert "2.68" in result.stdout
52
+
53
+
54
+ def test_bmv_vs_biva_selection(requests_mock):
55
+ """Test BMV vs BIVA selection logic (higher 'f' value wins)."""
56
+ # Create a response where BIVA has a more recent timestamp
57
+ response = {
58
+ "TESTSYMBOL": {
59
+ "bmv": {
60
+ "u": 100.0,
61
+ "p": 99.5,
62
+ "a": 98.0,
63
+ "x": 101.0,
64
+ "n": 97.0,
65
+ "c": 2.0,
66
+ "m": 2.0,
67
+ "v": 1000.0,
68
+ "o": 10.0,
69
+ "i": 100000.0,
70
+ "f": "2026-10-23 14:00:00",
71
+ },
72
+ "biva": {
73
+ "u": 105.0,
74
+ "p": 104.5,
75
+ "a": 103.0,
76
+ "x": 106.0,
77
+ "n": 102.0,
78
+ "c": 3.0,
79
+ "m": 3.0,
80
+ "v": 2000.0,
81
+ "o": 20.0,
82
+ "i": 200000.0,
83
+ "f": "2026-10-23 14:05:00", # More recent
84
+ },
85
+ }
86
+ }
87
+
88
+ requests_mock.get(
89
+ "https://api.databursatil.com/v2/cotizaciones",
90
+ json=response,
91
+ )
92
+
93
+ result = runner.invoke(app, ["TESTSYMBOL"], env={"COLUMNS": "200"})
94
+
95
+ assert result.exit_code == 0
96
+ # Should show BIVA data (105.0 ultimo) not BMV (100.0)
97
+ assert "105.0" in result.stdout
98
+ # BMV's ultimo (100.0) should NOT appear since we use BIVA
99
+ # But 100.0 could appear as part of other values, so check for the row pattern
100
+ assert "TESTSYMBOL" in result.stdout
@@ -0,0 +1,20 @@
1
+ import typer
2
+
3
+ from bmv_cli.commands.credits import app as credits
4
+ from bmv_cli.commands.currencies import app as currencies
5
+ from bmv_cli.commands.dividend import app as dividend
6
+ from bmv_cli.commands.hello import app as hello
7
+ from bmv_cli.commands.price import app as price
8
+ from bmv_cli.commands.search import app as search
9
+
10
+ app = typer.Typer()
11
+ app.add_typer(hello)
12
+ app.add_typer(price)
13
+ app.add_typer(currencies)
14
+ app.add_typer(credits)
15
+ app.add_typer(search)
16
+ app.add_typer(dividend)
17
+
18
+
19
+ if __name__ == "__main__":
20
+ app()